VUE的计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。Vue为了实现能够根据底层数据的变化反应性地执行副作用提供了两个助手: watch
和 watchEffect
。虽然这两个助手都可以监控响应性数据的变化,但它们的使用场景和行为有所不同。
watch
能够监视一个或多个响应性数据源,并在有更新时调用回调函数。
import { watch } from 'vue'
watch(source, callback, options?)
第一个参数是watch侦听的“数据源”,数据源可以是以下三种类型之一:
函数:一个获取器函数或一个返回值的计算属性
响应性对象:一个响应性对象
数组:以上任何一种的数组
第二个参数是当数据源发生变化时调用的回调函数,它接收以下参数:
value:被监视数据源的新值
oldValue:被监视数据源的旧值
onCleanup:一个可以用来注册清理回调的函数。在下一次 effect 重新运行之前,将会调用清理回调,清理无效的副作用,例如待处理的异步请求。
第三个参数是可选参数,一个选项对象,常用的选择是:
immediate:布尔值,表示是否应在创建观察者时立即触发回调。在第一次调用时,旧值将为 undefined
。
deep:布尔值,表示如果数据源是一个对象,是否执行深度遍历,以便在深度变化时触发回调。深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。其实如果给 watch
传入侦听的数据源是一个响应式对象,就已经隐式地创建一个深层侦听器。回调函数在所有嵌套的变更时都会被触发。
flush:字符串,用于指示如何调整回调的刷新时机。当你更改了被侦听的响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态(也就是副作用起效之前的状态)。如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,可以指明 flush: 'post'。
实现相似的功效还可以使用更方便的watchPostEffect
import { watchPostEffect } from 'vue'
watchPostEffect(() => { /* 在 Vue 更新后执行 */ })
watch
默认是懒加载,这意味着只有在被监视的源发生变化时,回调函数才会被调用。
watch可以观察多个源时,多个数据源形成数组,此时回调函数会接收到两个数组,这两个数组分别包含与源数组对应的新值和旧值。
// 多个来源组成的数组
watch([x, () => y.value],
([newX, newY]) => { console.log(`x is ${newX} and y is ${newY}`) })
相比于 watchEffect()
, watch()
使我们能够:
懒加载副作用
访问被监视状态的前一个和当前的值,这样可以更好的控制副作用执行的时机。
显式的说明哪种状态应触发观察者重新运行
const state = reactive({ count: 0 })
watch(
() => state.count,
(newCount, prevCount) => {
console.log(newCount, prevCount)
}
)
注意,你不能直接侦听响应式对象的属性值,例如:
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
const count = ref(0)
watch(count, (newCount, prevCount) => {
console.log(newCount, prevCount)
})
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
console.log("New values:" , foo, bar)
console.log("Old values:" , prevFoo, prevBar)
})
侦听器在回调函数中使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 todoId
的引用发生变化时使用侦听器来加载一个远程资源:
const todoId = ref(1)
const data = ref(null)
watch(todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)
data.value = await response.json()
},
{ immediate: true }
)
注意侦听器是如何两次使用 todoId
的,一次是作为源,另一次是在回调中。
可以用watchEffect侦听器来简化上面的代码。watchEffect()
允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:
const todoId = ref(1)
const data = ref(null)
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)
data.value = await response.json()
})
watchEffect中的回调会立即执行,不需要指定 immediate: true
。在执行期间,它会自动追踪 todoId.value
作为依赖(和计算属性类似)。每当 todoId.value
变化时,回调会再次执行。有了 watchEffect
,我们不再需要明确传递 todoId
作为源值。
再看一个watchEffect依赖reactive的例子:
import { reactive, watchEffect } from "vue"
const state = reactive({
count: 0,
name: 'Tom'
})
watchEffect(() => {
// 立即运行
console.log(`Count: ${state.count}, Name: ${state.name}`)
})
state.count++
state.name = 'Jerry'
在上述示例中,watchEffect
监控 state、count和name属性的变化。每当它们中的任何一个发生变化时,回调函数都会记录计数和名称的当前值。
首次调用 watchEffect
时,回调函数会立即以当前的 count
和 name
值执行。此后,只要任何响应性依赖项(count
或 name
)发生变化,回调函数就会重新运行。
就像 watch
一样, watchEffect
也有一些额外的功能,使其变得更加强大。
可以将选项对象作为第二个参数传递,以配置watchEffect的行为。例如,可以指定刷新时间或添加调试钩子。
回调函数会接收到一个名为 onCleanup
的特殊函数作为其第一个参数。使用此函数来注册一个清理回调,清理回调将在回调函数重新执行之前被调用。这对于清理不再需要的资源非常有用。
import { ref, watchEffect } from "vue"
const id = ref(1)
const data = ref(null)
watchEffect(async (onCleanup) => {
const { response, cancel } = await fetch(`https://example.com/api/data/${id.value}`)
onCleanup(cancel)
data.value = response.data
})
在上述示例中,我们使用 watchEffect
从API获取数据,每当id
属性发生变化时。使用 onCleanup
函数来注册一个取消函数,如果id
属性在请求完成之前发生变化,该函数将取消获取请求。
此外,可以使用 watchEffect
的返回值来停止侦听器。
import { watchEffect } from "vue"
const stop = watchEffect(() => {
// …
})
// Stop the watcher
stop()
最后有一个在官方文档中提及的watchEffec侦听器的t特点
watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。
以下的片段展示了这个问题:
注意,await
之前的所有内容(即 state.count
)都将被追踪。所有在 await
之后的内容(即 state.name
)将不会被追踪。
侦听器 watch
和 watchEffect
都允许我们以响应式的方式执行副作用,但它们在追踪依赖性方面有所不同。
Watch 只跟踪明确观察的源,不会跟踪回调内部访问的任何内容。此外,只有当数据源实际发生变化时,回调才会触发(回调的懒加载)。通过将依赖跟踪与副作用分离, watch
提供了对回调应何时触发的更精确控制。
WatchEffect 将依赖跟踪和副作用合并为一个阶段。在其同步执行过程中,它会自动跟踪访问的每一个响应性属性。这使得代码更加简洁,但使其响应性依赖关系变得不那么明显。
对于具有多个依赖项的观察者来说,使用watchEffect
可以消除手动维护依赖项列表的负担。
对于只有一个依赖项的情况来说,watchEffect
的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect
可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect
可能会比深度侦听器(deep watch)更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
但这是一把双刃剑。每次回调函数更新时,都需要进行关键性的思考,因为每一个新的添加都会被加入到依赖项中,并且当它发生变化时,将触发一个评估。
使用 watch
并明确你的依赖关系需要稍微多一些的努力,但可以确保更好的整体控制,将依赖跟踪与副作用分离,避免意外的性能降低。在我看来,使用 watch()
应被视为最佳实践。
本文参考了VUE3的官方文档和大迁世界公众号的相关内容。