资讯动态

Vue 3 深入学习笔记 03:watch、watchEffect 与响应式副作用

发布时间:2026/10/16 3:12:43 来源:尧图企业网站定制
Vue 3 深入学习笔记 03watch、watchEffect 与响应式副作用一、为什么需要 watch前面学习了ref、reactive和computed。它们分别解决不同的问题API主要用途示例ref定义响应式状态const count ref(0)reactive定义响应式对象const form reactive({ keyword: })computed根据已有状态计算新值const total computed(() price.value * count.value)watch监听指定数据的变化执行副作用搜索请求、保存设置watchEffect自动追踪执行过程中读取的响应式依赖根据多个状态自动执行逻辑这里要先理解一个重要概念副作用Side Effect是指除了计算并返回一个值之外还会影响外部状态或产生外部行为的操作。常见副作用包括调用后端 API。修改页面标题。将数据保存到localStorage。启动或清除定时器。操作第三方图表库。监听数据变化后执行其他业务逻辑。例如constcountref(0)constdoubleCountcomputed(()count.value*2)watch(count,(newValue,oldValue){console.log(count 发生变化)console.log(旧值,oldValue)console.log(新值,newValue)})这里computed负责计算doubleCount。watch负责监听count并在变化时执行回调。记忆重点computed主要用于派生数据watch主要用于响应变化并执行副作用。二、watch 的基本用法1. 监听 refimport{ref,watch}fromvueconstcountref(0)watch(count,(newValue,oldValue){console.log(旧值${oldValue}新值${newValue})})count.value1count.value2概念上的输出旧值0新值1 旧值1新值2watch的基本参数watch(source,callback,options)source监听的数据源。callback数据变化后执行的回调。options可选配置例如immediate、deep和flush。注意上面的输出描述的是两次修改分别被观察到时的典型结果。如果在同一轮同步代码中连续修改同一个 ref默认调度可能会将回调合并因此不一定每次赋值都执行一次回调。例如constcountref(0)watch(count,(newValue,oldValue){console.log(newValue,oldValue)})count.value1count.value2count.value3默认情况下多个同步修改通常会合并为一次回调输出3 0这也是理解 Vue 更新调度机制的重要基础。2. 监听 reactive 对象中的属性下面这种写法是错误的constformreactive({keyword:})watch(form.keyword,(newValue){console.log(newValue)})原因是form.keyword在传入watch时已经被读取。对于字符串等原始值传入的是当前值而不是能够持续追踪该属性的响应式数据源。正确方式是传入 getterwatch(()form.keyword,(newValue,oldValue){console.log(关键词变化,oldValue,-,newValue)})当form.keyword发生变化时Vue 会重新执行 getter 并检查监听值是否变化。也可以使用toRefconstkeywordtoRef(form,keyword)watch(keyword,(newValue,oldValue){console.log(oldValue,newValue)})3. 监听整个 reactive 对象constformreactive({keyword:,page:1})watch(form,(newValue,oldValue){console.log(表单数据发生变化)})form.keywordVue直接将reactive对象作为watch的数据源时Vue 默认会深度监听该对象的变化。但要注意对于深度监听的对象如果只是修改了对象内部属性回调中的newValue和oldValue通常是同一个代理对象。watch(form,(newValue,oldValue){console.log(newValueoldValue)})form.keywordVue输出true这是因为对象本身没有被替换变化的是对象内部的属性。如果业务需要比较修改前后的完整对象需要自己保存快照或者采用其他适合的数据比较方式。三、watch 的常用配置1. immediate立即执行一次默认情况下watch只在监听的数据源发生变化时执行回调不会因为创建监听器就立即执行。constkeywordref(Vue)watch(keyword,(newValue,oldValue){console.log(newValue,oldValue)},{immediate:true})初始化时就会执行一次回调。初次执行时newValue是当前值。oldValue是undefined。输出Vue undefined典型用途页面初始化时根据当前筛选条件请求数据。初始化时同步某些外部状态。希望初始化和后续变化共用同一套处理逻辑。2. deep深度监听默认情况下监听对象的 getter 通常只追踪它实际读取的依赖。若需要观察对象内部属性的变化可以使用深度监听。constuserref({name:Tom,profile:{age:20}})watch(user,(newValue,oldValue){console.log(用户数据发生变化)},{deep:true})user.value.profile.age21即使没有替换整个user.value内部的age变化也能触发回调。需要注意deep: true会递归追踪嵌套属性复杂对象可能带来额外开销。深度监听不等于自动生成修改前后的对象快照。如果只关心某一个字段通常直接监听该字段更清晰也更容易控制性能。Vue 3.5 及以上版本还支持将deep设置为数字以限制深度如果项目使用较早版本应确认当前版本是否支持该能力。3. flush控制回调执行时机flush用于控制watch回调相对于组件更新的执行时机。常用值配置含义常见场景pre默认值在所属组件 DOM 更新前执行一般业务监听post在所属组件 DOM 更新后执行需要访问更新后的 DOMsync数据变化时同步执行不走通常的批量调度少数特殊场景默认的 prewatch(source,callback,{flush:pre})适用于大多数业务逻辑。post更新 DOM 后执行watch(count,(){console.log(此时可以读取更新后的组件 DOM)},{flush:post})如果回调必须读取所属组件更新后的 DOM可以使用flush: post。也可以使用watchPostEffect()它相当于带有flush: post的watchEffect()。sync同步执行watch(count,(){console.log(同步执行)},{flush:sync})谨慎使用sync。如果一个数组或对象在短时间内连续发生多次修改同步监听可能频繁执行失去默认批量调度的优势。面试记忆普通监听优先使用默认时机依赖更新后的 DOM 时考虑post不要随意将所有监听器设置为sync。四、watchEffect自动追踪依赖1. 基本用法watch需要明确指定监听的数据源。而watchEffect会在执行回调时自动追踪其中读取的响应式数据。import{ref,watchEffect}fromvueconstfirstNameref(Tom)constlastNameref(Lee)watchEffect((){console.log(${firstName.value}${lastName.value})})初始化时会立即执行一次Tom Lee修改任意一个依赖firstName.valueJerry回调会重新执行输出Jerry Lee因为执行过程中读取了firstName.value和lastName.value所以两者都会成为该次执行所追踪的依赖。2. watch 与 watchEffect 的核心区别watchwatch(()form.keyword,(newValue,oldValue){console.log(newValue,oldValue)})开发者明确告诉 Vue监听form.keyword。watchEffectwatchEffect((){console.log(form.keyword)console.log(form.page)})Vue 根据回调执行过程中实际读取的响应式数据自动确定依赖。可以这样理解watch明确指定监听目标。watchEffect自动收集执行过程中读取的依赖。3. watchEffect 的依赖会动态变化constshowNameref(true)constnameref(Tom)constageref(20)watchEffect((){if(showName.value){console.log(name.value)}else{console.log(age.value)}})当showName为true时本次执行会读取showNamename此时修改age不会触发这个watchEffect。当执行条件切换为false后下一次执行会读取showNameage此时name不再是本次执行的依赖。这说明 Vue 的依赖收集不是简单地永久记录所有曾经读取过的数据而是会根据每次执行实际访问的响应式数据更新依赖关系。4. watchEffect 的一个重要细节异步函数watchEffect(async(){console.log(keyword.value)awaitfetchSomething()console.log(page.value)})对于watchEffect只有同步执行阶段读取的响应式数据会被自动追踪。因此上面的keyword.value会被追踪而await之后才读取的page.value不会因为这次读取而被自动追踪。这是因为await之后的代码会在异步阶段继续执行不再处于本次同步依赖收集阶段。如果某个数据必须成为明确的监听目标使用watch通常更直观。五、watch 和 watchEffect 应该怎么选场景一搜索框变化后请求 API推荐使用watch。constkeywordref()watch(keyword,async(newKeyword){if(!newKeyword.trim()){return}constresultawaitsearchApi(newKeyword)console.log(result)})原因明确只监听搜索关键词。可以取得新值和旧值。容易结合防抖、请求取消等机制控制行为。场景二根据多个状态同步页面标题可以考虑watchEffect。constfirstNameref(Tom)constlastNameref(Lee)watchEffect((){document.title${firstName.value}${lastName.value}})原因依赖简单自动追踪读取的状态比较方便。但如果这是实际 Vue 组件还需要考虑组件卸载时的清理以及是否有更合适的页面标题管理方式。场景三只在某个状态达到指定值时执行逻辑推荐使用watch。watch(isLoggedIn,(newValue){if(newValue){console.log(用户已登录)}})原因监听目标明确业务触发条件清晰。场景四初始化时执行并自动追踪所需数据可以考虑watchEffect。watchEffect((){console.log(form.keyword,form.page)})它会立即执行并自动追踪同步执行阶段读取的响应式数据。但如果涉及复杂请求、精确控制触发条件或旧值比较使用watch往往更容易维护。六、实战搜索框监听与请求竞态问题这部分非常重要因为实际项目中仅仅使用watch还不够。1. 基础实现constkeywordref()watch(keyword,async(newKeyword){constresultawaitsearchApi(newKeyword)searchResult.valueresult})假设用户快速输入V → Vu → Vue会产生多个请求请求 A搜索 V 请求 B搜索 Vu 请求 C搜索 Vue问题在于请求返回顺序不一定与发送顺序相同。例如请求 A 发出 请求 B 发出 请求 C 发出 请求 C 返回Vue 请求 B 返回Vu 请求 A 返回V如果每个请求返回后都直接更新结果最终页面可能展示V的搜索结果而不是用户最后输入的Vue。这就是请求竞态问题Race Condition。2. 方案一使用请求序号constkeywordref()constsearchResultref([])letrequestId0watch(keyword,async(newKeyword){constcurrentRequestIdrequestIdconstresultawaitsearchApi(newKeyword)if(currentRequestId!requestId){return}searchResult.valueresult})执行过程每次请求发出前递增请求序号。请求返回后检查当前请求序号是否仍然是最新的。如果不是最新请求就忽略结果。只有最新请求可以更新页面数据。这种方案可以避免旧请求覆盖新请求的结果但不会取消已经发出的旧请求。3. 方案二使用 AbortController 取消请求如果底层请求 API 支持AbortSignal可以尝试取消过期请求。constkeywordref()constsearchResultref([])watch(keyword,async(newKeyword,oldKeyword,onCleanup){constcontrollernewAbortController()onCleanup((){controller.abort()})try{constresultawaitfetch(/api/search?q${encodeURIComponent(newKeyword)},{signal:controller.signal})if(!result.ok){thrownewError(HTTP${result.status})}searchResult.valueawaitresult.json()}catch(error){if(error.nameAbortError){return}console.error(搜索失败,error)}})这里的onCleanup用于注册清理函数。当监听器即将因下一次执行而重新运行或者监听器停止时Vue 会调用相应清理逻辑从而取消之前的请求。注意取消请求要求底层请求 API 支持取消信号。取消并不保证服务器端一定停止处理请求。如果使用自定义 API 封装需要确认它是否支持传入AbortSignal。实际项目还应根据需求处理加载状态、错误提示和空关键词。4. 两种方案如何选择方案优点注意点请求序号实现简单可以防止旧结果覆盖新结果旧请求仍可能继续执行AbortController能够取消支持取消信号的请求减少不必要的客户端工作需要底层 API 支持并正确处理取消异常两者结合取消旧请求同时防止旧结果更新实现略复杂但在复杂请求场景中更稳妥七、watch 的停止与清理1. 停止监听通常在组件的setup()或script setup中同步创建的watch和watchEffect会自动关联到当前组件并在组件卸载时停止。也可以接收返回的停止函数conststopwatch(count,(newValue){console.log(newValue)})// 需要时手动停止stop()2. 异步创建监听器需要注意setTimeout((){watchEffect((){console.log(count.value)})},1000)如果监听器是在组件setup()执行之后通过定时器、异步回调等方式创建的它通常不会自动关联到原组件的生命周期。这种情况下需要自己保存停止函数并在合适的时机调用避免不必要的监听器长期存在。实际项目中如果只是希望延迟开始处理通常更推荐在组件初始化时同步创建监听器再通过状态控制其是否执行。八、常见误区总结误区一watch 一定会在每次赋值时执行一次错误。默认watch会通过调度队列批量处理更新。同一轮同步代码中的多次修改可能被合并。误区二watch(form.keyword) 可以监听 reactive 的属性错误。如果form.keyword是字符串、数字等原始值直接传入会得到当时的值。应该使用watch(()form.keyword,callback)或者watch(toRef(form,keyword),callback)误区三watch 和 watchEffect 没有区别错误。watch明确指定监听目标并提供新值和旧值。watchEffect自动追踪同步执行阶段读取的依赖创建后立即执行。误区四deep: true 可以拿到修改前的对象快照错误。深度监听能够追踪嵌套属性变化但嵌套属性被修改时newValue和oldValue可能仍然指向同一个对象。误区五使用 async watchEffect 就能追踪所有读取的数据错误。watchEffect只自动追踪同步执行阶段读取的响应式数据。await之后的读取不会因为本次读取自动成为依赖。误区六watch 回调中发请求就不会出现问题错误。异步请求可能乱序返回。需要根据业务使用请求序号、请求取消或其他竞态控制机制。九、本节总结watch适合明确监听指定数据并执行副作用。watchEffect适合自动追踪同步执行阶段读取的响应式数据。immediate控制是否立即执行deep控制是否深度追踪flush控制回调相对于组件更新的执行时机。Vue 会批量调度常规更新因此多次同步修改不一定对应多次watch回调。异步请求需要考虑竞态、取消和错误处理。同步创建的组件监听器通常会随组件卸载自动停止异步创建的监听器需要特别注意清理。十、Vue 面试常见问题与参考答案问题 1watch 和 computed 有什么区别参考答案computed用于根据已有响应式数据计算派生值具有缓存机制依赖没有变化时通常不会重新计算。watch用于监听指定数据的变化并执行副作用例如请求 API、保存数据或执行其他业务逻辑。简单来说computed主要解决数据计算问题watch主要解决变化后的行为问题。问题 2watch 和 watchEffect 有什么区别参考答案watch需要明确指定监听数据源可以获得新值和旧值并且默认不会立即执行回调。watchEffect会在执行时自动追踪同步读取的响应式数据创建后立即执行但没有直接提供与watch相同的新值、旧值回调参数。当需要明确控制监听目标、比较新旧值或精确管理请求触发条件时优先考虑watch当依赖简单且希望自动追踪时可以考虑watchEffect。问题 3watch 为什么默认不会立即执行参考答案watch默认关注的是监听数据源发生变化后的行为而不是创建监听器时的初始化处理。如果希望创建监听器时立即执行一次可以设置watch(source,callback,{immediate:true})此时第一次回调的oldValue通常为undefined。问题 4watch 如何监听 reactive 对象的某个属性参考答案使用 getterwatch(()form.keyword,(newValue,oldValue){console.log(newValue,oldValue)})因为 getter 在监听执行时读取响应式属性Vue 可以追踪这个属性的依赖。也可以使用toRef(form, keyword)创建与该属性保持关联的 Ref再将 Ref 传给watch。问题 5watch 的 deep 有什么作用有没有性能问题参考答案deep用于追踪对象嵌套属性的变化。开启深度监听后Vue 需要遍历并追踪相关嵌套属性因此复杂对象可能产生额外开销。如果只关心特定字段优先直接监听该字段而不是对整个复杂对象开启深度监听。问题 6watch 的 flush 有哪些取值参考答案常用值有pre默认时机在所属组件 DOM 更新前执行。post在所属组件 DOM 更新后执行。sync同步执行不经过通常的批量调度。如果回调需要访问更新后的 DOM可以使用flush: post。sync可能在频繁修改时导致回调执行过多应谨慎使用。问题 7watchEffect 为什么只能追踪 await 之前读取的数据参考答案watchEffect通过执行回调来自动收集依赖只有同步执行阶段读取的响应式数据会被追踪。await之后的代码会在异步阶段继续执行不属于本次同步依赖收集阶段。因此异步回调中需要明确监听目标时通常使用watch更容易控制。问题 8如何避免搜索请求乱序返回导致页面数据错误参考答案可以使用请求序号或AbortController。请求序号方案会在每次请求前更新序号只有最新请求返回后才能更新页面数据。AbortController可以取消支持取消信号的旧请求减少不必要的工作。复杂场景下可以同时使用两种方案并处理取消异常和普通请求错误。问题 9组件卸载后watch 会自动停止吗参考答案在组件setup()或script setup中同步创建的watch和watchEffect通常会与组件生命周期关联在组件卸载时自动停止。如果监听器是在后续异步回调中创建的则通常需要自己保存停止函数并在合适的时机调用以避免监听器泄漏或执行不必要的逻辑。问题 10修改响应式数据后watch 回调为什么不一定立即执行参考答案Vue 默认会调度并批量处理常规监听和组件更新。连续的同步修改可能被合并为一次回调执行。如果业务确实需要同步执行可以考虑flush: sync但它可能增加执行次数。若只是需要等待 DOM 更新完成通常使用nextTick()或flush: post更合适。十一、本节自测题题目 1watch 的执行次数constcountref(0)watch(count,(newValue,oldValue){console.log(newValue,oldValue)})count.value1count.value2count.value3默认情况下通常会输出几次输出什么为什么题目 2watch 与 watchEffectconstaref(1)constbref(2)watch(a,(){console.log(watch)})watchEffect((){console.log(a.valueb.value)})b.value3哪些回调会执行为什么题目 3搜索请求竞态用户快速输入V、Vu、Vue三个请求的返回顺序依次为Vue、Vu、V。如果每次请求完成后都直接更新searchResult最终可能出现什么问题请分别用请求序号和AbortController说明如何解决。自测题答案问题1控制台输出3 0watch 回调通常只执行一次。Vue 默认会对 watch 回调进行批量调度。同一轮同步代码中连续修改 count 三次回调通常只执行一次。执行时newValue 为最终值 3oldValue 为变化前的值 0。问题2控制台输出3和4watch 回调不会执行watchEffect 执行两次。watchEffect 创建后会立即执行一次此时 a 1、b 2因此输出 3。执行过程中Vue 自动追踪了 a 和 b。当 b 修改为 3 时watchEffect 再次执行输出 4。watch 只监听 a而本题没有修改 a因此 watch 回调不会执行。问题3最终页面可能显示搜索 V 的结果而不是最新关键词 Vue 的结果。可以通过请求序号判断请求是否过期只允许最新请求更新页面。可以使用 AbortController 尝试取消旧请求减少无效请求处理。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑