侦听器
基本示例
计算属性允许我们声明性地计算派生值。然而,在有些情况下,我们需要对状态的变化展现出犹如有 "副作用" 一般的反应,例如更改 DOM,或基于某异步操作其他状态。
在组合式 API 中,我们可以使用 watch
方法 让每次响应式状态变化时都触发一个回调函数执行:
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('问句通常都会带一个问号。;-)')
// 直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
answer.value = '思考中...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (e) {
answer.value = '出错了!无法访问该 API。' + error
}
}
})
</script>
<template>
<p>
提一个 Yes/No 的问题:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
侦听来源类型
watch
的第一个参数可以是不同类型的响应式 “源”:它可以是一个 ref(包括计算属性),一个响应式对象,一个函数,或是一个数组表示多个源:
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// 函数
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}`)
})
注意,你不能像这样观察一个响应式对象的属性:
const obj = reactive({ count: 0 })
// 这不会正常工作,因为你是向 watch() 传入了一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
此时你应该传入一个函数:
// 提供一个获取函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
深层侦听器
当你直接对一个响应式对象调用 watch()
,会隐式地创建一个深层侦听器,回调会在每个层级更改时都被触发:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在深层次属性更改时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
这应该与返回响应式对象的函数有所区别,在后一种情况下,只有在函数返回不同的对象时才会触发回调:
watch(
() => state.someObject,
() => {
// 仅当 state.activeObject 被替换时触发
}
)
You can, however, force the second case into a deep watcher by explicitly using the deep
option:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
谨慎使用
深度观察需要遍历被观察对象中的所有深层属性,该操作当用于大型数据结构时可能会很昂贵。因此请只在必要时才使用它,并且要注意其性能影响。
watchEffect()
watch()
是懒执行的:回调函数只有在侦听的源更改时才会调用。但某些场景下我们可能希望回调函数能呈积极态调用。举个例子,我们可能会请求一些初始数据,然后在相应状态改变时重新请求。我们可以这样来写:
const url = ref('https://...')
const data = ref(null)
async function fetchData() {
const response = await fetch(url.value)
data.value = await response.json()
}
// 立即获取
fetchData()
// ...再侦听 url 变化
watch(url, fetchData)
这可以通过 watchEffect
方法 来简化,watchEffect()
使我们可以立即执行一次该副作用,并自动追踪依赖。上面的例子可以重写为:
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
上面这个例子中,回调会立即执行一次。在执行期间,它会自动追踪 url.value
作为依赖(近似于计算属性)。每当 url.value
变化时,回调将会再次执行。
你可以查看这个使用 watchEffect
的 这个例子,了解如何在运行时做响应式数据请求。
TIP
watchEffect
仅会在其 同步 执行期间追踪依赖。当使用一个异步回调时,只有在第一次 await
前被访问的属性会被追踪为依赖。
watch
vs. watchEffect
watch
和 watchEffect
都给我们提供了创建副作用的能力。它们之间的主要区别是追踪响应式依赖的方式:
watch
只跟踪明确监视的源。它不会跟踪任何在回调中访问到的东西。另外,回调仅会在源确实改变了才会被触发,watch
将依赖追踪和副作用区分开,这让我们对如何触发回调有更多的控制权。而
watchEffect
则将依赖追踪和副作用耦合,会自动追踪其同步执行过程中访问到的所有响应式属性。这更方便,一般来说代码也会更简洁,但其响应性依赖关系则不那么显式。
副作用刷新时机
当你更改了响应式状态,可能同时触发 Vue 组件更新和你定义的监视器回调。
默认情况下,用户创建的副作用都会在 Vue 组件更新的副作用 之前 被调用。这意味着,如果你试图在监视器回调中访问 DOM, DOM 将是 Vue 执行任何更新之前的状态。
如果你想于 Vue 更新之后,在侦听器回调中访问 DOM,你需要指明 flush: 'post'
选项:
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
后置刷新的 watchEffect()
也有一个更便捷的别名 watchPostEffect()
:
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
停止侦听器
在 setup()
或 <script setup>
同步声明的侦听器会和宿主组件绑,也会在组件卸载时自动停止,在大多数场景下你无需关心要怎么操作来停止它。
一个关键点是,侦听器必须是被 同步 创建的:如果侦听器是在异步回调中被创建的,它将不会绑定当前组件为宿主,并且必须手动停止以防内存泄漏,如下方这个例子所示:
<script setup>
import { watchEffect } from 'vue'
// 这个副作用会在组件卸载时自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要手动停止一个侦听器,请使用返回的处理函数。watch
和 watchEffect
都是这样:
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
注意,需要异步创建监视器的情况应该很少,并且应该尽可能首选同步创建。如果需要等待一些异步数据,可以将侦听逻辑设置为有条件的:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 得到数据后要做的事...
}
})