文中写法均为组合式。
watch()
需要在某些状态改变时,执行一些操作,如:进行异步操作接口获取、修改另一处状态等,就需要用到侦听器watch。
例子:
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
// 可以直接侦听一个 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>
上面例子中当输入框内含?时,底下的显示内容会在调取接口https://yesno.wtf/api,根据随机的返回值显示yes或no。
监听的数据源类型
watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
第一个参数是侦听器的源。这个来源可以是以下几种:
- 一个函数,返回一个值
- 一个 ref
- 一个响应式对象
- ...或是由以上类型的值组成的数组
例子:
const x = ref(0)
const y = ref(0)
const obj = reactive({ count: 0 })
// 一个函数,返回一个值
watch(() => obj.count, (count) => {
console.log(`Count is: ${count}`)
})
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// 一个响应式对象,自动启用深层模式
watch(obj, () => {
/* 深层级变更状态所触发的回调 */
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
Tips:
当使用 getter 函数作为源时,回调只在此函数的返回值变化时才会触发。如果你想让回调在深层级变更时也能触发,你需要使用 { deep: true } 强制侦听器进入深层级模式。
第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
第三个可选的参数是一个对象,支持以下这些选项:
- immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined。
- deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考侦听器 | Vue.js。
- flush:调整回调函数的刷新时机。参考回调的刷新时机(侦听器 | Vue.js)及 下方的watchEffect()。
- onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器(深入响应式系统 | Vue.js)深入响应式系统 | Vue.js。
- once:回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。(3.4+以上支持)
与 watchEffect() 相比,watch() 的特点是:
- 懒执行副作用;
- 更加明确是应该由哪个状态触发侦听器重新执行;
- 可以访问所侦听状态的前一个值和当前值。
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()
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
watchEffect(
(cleanupFn: () => void) =>{},
{
flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
)
- 第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求 (参见下面的示例)。
- 第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖。
watchEffect() 使用 flush: 'post' 选项时的别名:watchPostEffect()。
watchEffect() 使用 flush: 'sync' 选项时的别名:watchSyncEffect()。
默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。
设置 flush: 'post' 将会使侦听器延迟到组件渲染之后再执行。详见回调的触发时机。在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置 flush: 'sync' 来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。
返回值是一个用来停止该副作用的函数。
应用方式与watch无差别,可用来简化watch:
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 }
)
使用watchEffect()简化
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
Tips:
watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。
watch vs. watchEffect的区别
watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
- watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
- watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。