监听器
基本示例
计算属性允许我们声明式地计算派生值。然而,在某些情况下,我们需要根据状态变化执行“副作用” - 例如,修改 DOM,或根据异步操作的结果更改另一部分状态。
使用 Composition API,我们可以使用 watch
函数 来触发回调,当任何响应式状态变化时
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
// watch works directly on a ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
监听源类型
watch
的第一个参数可以是不同类型的响应式“源”:它可以是一个 ref(包括计算 ref)、一个响应式对象、一个 getter 函数,或者多个源的数组
js
const x = ref(0)
const y = ref(0)
// single ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// array of multiple sources
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
请注意,您不能像这样监听响应式对象的属性
js
const obj = reactive({ count: 0 })
// this won't work because we are passing a number to watch()
watch(obj.count, (count) => {
console.log(`Count is: ${count}`)
})
相反,请使用 getter
js
// instead, use a getter:
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)
深度监听器
当你直接在响应式对象上调用 watch()
时,它将隐式创建一个深度监听 - 回调将在所有嵌套变化上触发。
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// fires on nested property mutations
// Note: `newValue` will be equal to `oldValue` here
// because they both point to the same object!
})
obj.count++
这应该与返回响应式对象的 getter 进行区分 - 在后一种情况下,只有当 getter 返回不同的对象时,回调才会触发。
js
watch(
() => state.someObject,
() => {
// fires only when state.someObject is replaced
}
)
然而,你可以通过显式使用 deep
选项将第二种情况强制转换为深度监听。
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// Note: `newValue` will be equal to `oldValue` here
// *unless* state.someObject has been replaced
},
{ deep: true }
)
在 Vue 3.5+ 中,deep
选项也可以是一个数字,表示最大遍历深度 - 即 Vue 应该遍历对象嵌套属性多少层。
谨慎使用
深度监听需要遍历被监听对象的所有嵌套属性,在大型数据结构中使用时可能会很昂贵。仅在必要时使用,并注意性能影响。
急切监听器
watch
默认为惰性:回调不会在被监听源改变时调用。但在某些情况下,我们可能希望相同的回调逻辑被急切执行 - 例如,我们可能想要获取一些初始数据,然后每当相关状态改变时重新获取数据。
我们还可以通过传递 immediate: true
选项来强制执行一个监听器的回调,立即执行。
js
watch(
source,
(newValue, oldValue) => {
// executed immediately, then again when `source` changes
},
{ immediate: true }
)
一次性监听器
- 仅支持 3.4+
监听器的回调会在被监听源改变时执行。如果你想回调在被源改变时只触发一次,请使用 once: true
选项。
js
watch(
source,
(newValue, oldValue) => {
// when `source` changes, triggers only once
},
{ once: true }
)
watchEffect()
监听器的回调通常使用与源完全相同的响应式状态。例如,考虑以下代码,它使用一个监听器在 todoId
ref 变化时加载远程资源。
js
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()
允许我们自动跟踪回调的响应式依赖。上面的监听器可以重写为
js
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
在这里,回调将立即运行,无需指定 immediate: true
。在其执行过程中,它将自动跟踪 todoId.value
作为依赖(类似于计算属性)。每当 todoId.value
变化时,回调将再次运行。使用 watchEffect()
,我们不再需要显式地传递 todoId
作为源值。
你可以查看 这个 watchEffect()
和响应式数据获取的例子。
对于只有一个依赖项的示例,使用 watchEffect()
的好处相对较小。但对于具有多个依赖项的监视器,使用 watchEffect()
可以消除手动维护依赖项列表的负担。此外,如果您需要在嵌套数据结构中监视多个属性,watchEffect()
可能比深层次监视器更高效,因为它只会跟踪回调中使用的属性,而不是递归地跟踪所有属性。
提示
watchEffect
只在其 同步 执行期间跟踪依赖项。当与异步回调一起使用时,只有在前一个 await
调用之前访问的属性才会被跟踪。
watch
与 watchEffect
watch
和 watchEffect
都允许我们进行反应式副作用。它们的主要区别在于它们跟踪反应式依赖项的方式。
watch
只跟踪显式监视的源。它不会跟踪回调内部访问的内容。此外,回调仅在源实际更改时触发。watch
将依赖项跟踪与副作用分开,使我们能够更精确地控制回调何时触发。另一方面,
watchEffect
将依赖项跟踪和副作用合并为一个阶段。它将自动跟踪其在同步执行期间访问的每个反应式属性。这更方便,通常会产生更简洁的代码,但使得其反应式依赖项不够明确。
副作用清理
有时我们可能在监视器中执行副作用,例如异步请求。
js
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// callback logic
})
})
但如果在请求完成之前 id
发生了变化怎么办?当先前的请求完成时,它仍然会使用已经过时的 ID 值触发回调。理想情况下,我们希望在 id
变为新值时取消过时的请求。
我们可以使用 onWatcherCleanup()
API 来注册一个清理函数,该函数将在监视器被无效化和即将重新运行时被调用
js
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// callback logic
})
onWatcherCleanup(() => {
// abort stale request
controller.abort()
})
})
请注意,onWatcherCleanup
仅在 Vue 3.5+ 中受支持,并且必须在 watchEffect
效果函数或 watch
回调函数的同步执行期间调用:您不能在异步函数中的 await
语句之后调用它。
或者,一个 onCleanup
函数也被作为第三个参数传递给监视器回调,并且作为第一个参数传递给 watchEffect
效果函数
js
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
这在版本 3.5 之前有效。此外,通过函数参数传递的 onCleanup
绑定到监视器实例,因此它不受 onWatcherCleanup
的同步约束。
回调刷新时机
当您修改反应式状态时,它可能会触发 Vue 组件更新和您创建的监视器回调。
类似于组件更新,用户创建的监视器回调被批量处理以避免重复调用。例如,如果我们同步地将一千个项目推送到被监视的数组中,我们可能不希望监视器触发一千次。
默认情况下,监视器的回调函数将在父组件更新(如果有)之后、以及所属组件的DOM更新之前被调用。这意味着如果您在监视器回调函数中尝试访问所属组件的自身DOM,DOM将处于更新前的状态。
后置监视
如果您想在Vue更新所属组件的DOM之后在监视器回调中访问所属组件的DOM,您需要指定flush: 'post'
选项
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
后置刷新watchEffect()
还有一个便利别名,watchPostEffect()
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* executed after Vue updates */
})
同步监视
也可以创建一个在Vue管理的任何更新之前触发的同步监视器
js
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
同步watchEffect()
还有一个便利别名,watchSyncEffect()
js
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* executed synchronously upon reactive data change */
})
谨慎使用
同步监视器没有批处理,并且在检测到每次响应式变异时都会触发。它们可以用来监视简单的布尔值,但避免在可能同步多次变异的数据源上使用,例如数组。
停止监视器
在setup()
或<script setup>
内部同步声明的监视器绑定到所属组件实例,并在所属组件卸载时自动停止。在大多数情况下,您不需要担心手动停止监视器。
关键在于监视器必须同步创建:如果监视器在异步回调中创建,它将不会绑定到所属组件,并且必须手动停止以避免内存泄漏。以下是一个示例
vue
<script setup>
import { watchEffect } from 'vue'
// this one will be automatically stopped
watchEffect(() => {})
// ...this one will not!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要手动停止监视器,请使用返回的句柄函数。这对于watch
和watchEffect
都适用
js
const unwatch = watchEffect(() => {})
// ...later, when no longer needed
unwatch()
请注意,在极少数情况下需要创建异步监视器,并且尽可能优先使用同步创建。如果您需要等待一些异步数据,您可以将您的监视逻辑条件化
js
// data to be loaded asynchronously
const data = ref(null)
watchEffect(() => {
if (data.value) {
// do something when data is loaded
}
})