侦听器
侦听器
watch
基础
官方文档:响应式 API:核心 | Vue.js (vuejs.org)
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
详细信息
watch()
默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
第一个参数是侦听器的源。这个来源可以是以下几种:
一个函数,返回一个值
一个 ref
一个响应式对象
...或是由以上类型的值组成的数组
第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
- 当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
第三个可选的参数是一个对象,支持以下这些选项:
immediate
:在侦听器创建时立即触发回调。第一次调用时旧值是undefined
。
deep
:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。
flush
:调整回调函数的刷新时机。参考回调的刷新时机及watchEffect()
。
onTrack / onTrigger
:调试侦听器的依赖。参考调试侦听器。
once
:回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。与
watchEffect()
相比,watch()
使我们可以:
- 懒执行副作用;
- 更加明确是应该由哪个状态触发侦听器重新执行;
- 可以访问所侦听状态的前一个值和当前值。
示例
侦听一个 getter 函数:
const state = reactive({ count: 0 }) watch( () => state.count, (count, prevCount) => { /* ... */ } )
侦听一个 ref:
const count = ref(0) watch(count, (count, prevCount) => { /* ... */ })
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */ })
当使用 getter 函数作为源时,回调只在此函数的返回值变化时才会触发。如果你想让回调在深层级变更时也能触发,你需要使用
{ deep: true }
强制侦听器进入深层级模式。在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。const state = reactive({ count: 0 }) watch( () => state, (newValue, oldValue) => { // newValue === oldValue }, { deep: true } )
当直接侦听一个响应式对象时,侦听器会自动启用深层模式:
const state = reactive({ count: 0 }) watch(state, () => { /* 深层级变更状态所触发的回调 */ })
watch()
和watchEffect()
享有相同的刷新时机和调试选项:watch(source, callback, { flush: 'post', onTrack(e) { debugger }, onTrigger(e) { debugger } })
停止侦听器:
const stop = watch(source, callback) // 当已不再需要该侦听器时: stop()
副作用清理:
watch(id, async (newId, oldId, onCleanup) => { const { response, cancel } = doAsyncWork(newId) // 当 `id` 变化时,`cancel` 将被调用, // 取消之前的未完成的请求 onCleanup(cancel) data.value = await response })
原理
watchEffect()
基础
官方文档:响应式 API:核心 | Vue.js (vuejs.org)
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
详细信息
第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求 (参见下面的示例)。
第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖。
默认情况下,侦听器将在组件渲染之前执行。设置
flush: 'post'
将会使侦听器延迟到组件渲染之后再执行。详见回调的触发时机。在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置
flush: 'sync'
来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。返回值是一个用来停止该副作用的函数。
示例
const count = ref(0) watchEffect(() => console.log(count.value)) // -> 输出 0 count.value++ // -> 输出 1
副作用清除:
watchEffect(async (onCleanup) => { const { response, cancel } = doAsyncWork(id.value) // `cancel` 会在 `id` 更改时调用 // 以便取消之前 // 未完成的请求 onCleanup(cancel) data.value = await response })
停止侦听器:
const stop = watchEffect(() => {}) // 当不再需要此侦听器时: stop()
选项:
watchEffect(() => {}, { flush: 'post', onTrack(e) { debugger }, onTrigger(e) { debugger } })
参考
为什么watchEffect 会自动追踪所有能访问到的响应式属性
Vue 3 中的 watchEffect
之所以会自动追踪所有能访问到的响应式属性,主要基于以下几个原因和机制:
- 基于 Proxy 的响应式系统:
- Vue 3 使用了 Proxy 对象来实现其响应式系统。当 Vue 将一个普通对象转换为响应式对象时,它实际上是在该对象上包裹了一个 Proxy 代理。
- Proxy 允许 Vue 拦截对象属性的读取(get 操作)和修改(set 操作)。在读取属性时,Vue 会进行依赖收集;在修改属性时,Vue 会触发依赖更新。
- 依赖收集:
- 当
watchEffect
的回调函数被调用时,Vue 会开始追踪该函数中访问的所有响应式属性。这是因为watchEffect
的回调函数在执行过程中,会触发这些响应式属性的 get 拦截器,从而允许 Vue 收集这些依赖。 - 这些被收集的依赖会形成一个依赖关系图,Vue 会使用这个图来确定哪些副作用(即
watchEffect
的回调函数)需要在响应式数据变化时重新执行。
- 当
- 副作用函数的执行:
watchEffect
的回调函数被视为一个副作用函数。在 Vue 的组件渲染过程中,这些副作用函数会被执行,并在此过程中收集依赖。- 由于 Vue 使用了 Proxy 进行依赖收集,因此
watchEffect
的回调函数可以访问到的所有响应式属性都会被自动追踪。
- 响应式数据的更新:
- 当响应式数据发生变化时(即触发 set 拦截器),Vue 会查找与该数据相关的所有依赖,并触发这些依赖的重新执行。
- 在这种情况下,由于
watchEffect
的回调函数已经收集了这些响应式数据的依赖,因此当这些数据发生变化时,watchEffect
的回调函数会被重新执行。
watch 和 watchEffect 区别
在 Vue 3 中,watch
和 watchEffect
都是用于监听响应式数据的变化并在变化时执行特定逻辑的工具,但它们之间存在一些明显的区别:
追踪响应式依赖的方式
watch
:只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西,除非在配置中明确指定。这意味着它可以更加精确地控制哪些数据变化会触发回调函数。watchEffect
:会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这使得代码更简洁,但依赖关系可能不那么明确。
执行时机
watch
:默认情况下是惰性的,即在数据源确实改变时才会触发回调。但可以通过配置immediate: true
使其在页面加载时立即执行一次。watchEffect
:是立即执行的,它在页面加载时会主动执行一次来收集依赖。
参数
watch
:至少需要两个参数。第一个是要监听的数据(可以是单个值、数组或函数返回的值),第二个是回调函数。还可以有第三个参数,即配置项对象。watchEffect
:只需要一个参数,即回调函数。它不需要明确指定要监听的数据,而是自动追踪在回调函数中使用的响应式数据。
访问变化前后的值
watch
:回调函数内会返回最新值和修改之前的值。watchEffect
:只能访问当前最新的值,无法访问到修改之前的值。
停止观察
watch
:返回的函数可以用来停止观察。watchEffect
:也有返回值,它是一个无需停止的监听器函数。如果需要停止监听,可以调用这个监听器函数来停止监听。
深度观察
watch
:默认是浅观察的,但可以通过配置deep: true
使其能够深度观察对象或数组内部的嵌套属性变化。watchEffect
:自动追踪函数中使用的响应式数据,因此也会跟踪嵌套属性的变化。
使用场景
watch
:适合需要精确控制监听哪些数据变化,并需要访问变化前后值的场景。watchEffect
:适合不需要明确指定监听哪些数据,只需要在数据变化时执行副作用函数的场景。其代码更简洁,响应性依赖关系更自动。