Vue 3 watch 与 watchEffect:异步请求、深度监听和清理
Vue 3 watch 与 watchEffect:异步请求、深度监听和清理
类型:Vue 官方 Watchers 文档中文解读。示例保持官方 API 的含义,并补充请求竞态的解释;没有把官方示例宣称为本项目实测。Vue 版本差异会影响可用选项,尤其是
onWatcherCleanup()与once。[3]
什么时候用 watcher
计算属性适合从已有状态得到一个派生值。watcher 用来在状态变化后执行副作用,例如请求网络、修改 DOM 或更新另一个外部系统。Vue 官方把这两类工作区分开:不要为了计算显示值而滥用 watcher,也不要把异步副作用硬塞进 computed。[3]
watch:显式声明依赖
Composition API 中,watch 的第一个参数是明确的监听源,第二个参数是回调。它不会自动追踪回调内部访问的其他响应式变量,因此触发条件更容易检查。
下面保留官方示例的核心行为:只有问题里出现问号时才请求接口,并在请求期间更新 loading 状态。
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
watch(question, async (newQuestion) => {
if (!newQuestion.includes('?')) {
return
}
loading.value = true
answer.value = 'Thinking...'
try {
const response = await fetch('https://yesno.wtf/api')
answer.value = (await response.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:</p>
<input v-model="question" :disabled="loading" />
<p>{{ answer }}</p>
</template>
watch 的监听源可以是 ref、computed ref、getter、reactive 对象或多个源组成的数组。[3]
import { reactive, ref, watch } from 'vue'
const x = ref(0)
const y = ref(0)
const state = reactive({ count: 0 })
watch(x, (newX) => {
console.log('x:', newX)
})
watch(() => x.value + y.value, (sum) => {
console.log('sum:', sum)
})
watch([x, () => y.value], ([newX, newY]) => {
console.log(newX, newY)
})
// 不能直接把 reactive 对象的当前数值作为监听源
// watch(state.count, callback)
// 应该传 getter
watch(() => state.count, (count) => {
console.log('count:', count)
})
最后一组写法很重要:watch(state.count, ...) 在调用时拿到的是一个普通数值,Vue 没有可追踪的 getter;watch(() => state.count, ...) 才保留了依赖关系。
watchEffect:自动收集同步执行期间的依赖
当回调使用的响应式状态和触发条件完全相同时,watchEffect 可以减少重复声明。它会立即执行,并在同步执行期间自动记录访问到的响应式依赖。[3]
<script setup>
import { ref, watchEffect } from 'vue'
const todoId = ref(1)
const data = ref(null)
const error = ref(null)
watchEffect(async () => {
error.value = null
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
data.value = await response.json()
})
</script>
异步回调有一个容易忽略的边界:await 之后访问的响应式数据不属于首次同步依赖收集范围。依赖应尽量在第一次 await 之前明确读取,或者改用显式的 watch。[3]
选择方法时可以这样判断:
- 需要精确知道哪个状态变化才触发:
watch。 - 回调自然使用少量响应式依赖,且希望立即运行:
watchEffect。 - 需要从状态计算一个值:优先 computed,而不是 watcher。
深度监听有成本
watch 默认是浅层监听。替换被监听的值会触发回调,但嵌套对象内部变更不一定触发。直接监听 reactive 对象时,Vue 会隐式建立深度监听;getter 返回一个 reactive 对象时,只有返回对象本身变化才触发,除非显式设置 deep: true。[3]
import { reactive, watch } from 'vue'
const form = reactive({
profile: {
name: ''
}
})
watch(form, () => {
// form.profile.name 修改时也会触发
})
watch(
() => form.profile,
() => {
// 默认更关注 profile 引用本身是否替换
},
{ deep: true }
)
深度监听要遍历嵌套属性,大对象上可能产生明显开销。先缩小监听源,只有确实需要时才使用深度监听。Vue 3.5 及以后还支持将 deep 写成最大遍历深度,但项目应先核对实际 Vue 版本。[3]
防止旧请求覆盖新结果
搜索框、路由参数和详情页经常出现这种竞态:用户先请求 A,随后快速切换到 B;如果 A 比 B 晚返回,旧结果可能覆盖新页面。
Vue 官方提供了 watcher 清理机制。onWatcherCleanup() 在 Vue 3.5+ 可用,而且必须在回调同步执行阶段注册;也可以使用 watcher 回调的第三个 onCleanup 参数,它不受同样的同步限制。[3]
import { ref, watch } from 'vue'
const id = ref(1)
const data = ref(null)
const error = ref(null)
watch(id, (newId, _oldId, onCleanup) => {
const controller = new AbortController()
let active = true
fetch(`/api/items/${newId}`, { signal: controller.signal })
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
})
.then((value) => {
if (active) {
data.value = value
}
})
.catch((reason) => {
if (reason.name !== 'AbortError' && active) {
error.value = reason
}
})
onCleanup(() => {
active = false
controller.abort()
})
})
AbortController 取消网络请求,active 又避免了某些非 fetch 副作用在清理后继续写入状态。生产代码还应处理重试、缓存、鉴权和组件离开后的用户体验,这些不是 watcher API 自动提供的。
回调执行时机
Vue 会对 watcher 回调进行批处理,避免同步修改多次就重复执行很多次。默认回调发生在父组件更新后、所属组件 DOM 更新前。如果需要访问更新后的组件 DOM,可以使用 flush: 'post';flush: 'sync' 会同步触发,但没有批处理,不适合频繁变化的数组等数据。[3]
import { watch } from 'vue'
watch(source, callback, {
flush: 'post'
})
这不是单纯的性能开关。它改变了回调观察到的 DOM 时点,读取 DOM 前必须明确选择时机。
watcher 的生命周期
同步创建于组件 setup() 或 <script setup> 中的 watcher 会绑定到组件,在组件卸载时自动停止。若在异步回调中创建 watcher,它不会自动绑定到组件,必须手动保存并停止,否则可能产生内存泄漏。[3]
import { onUnmounted, watchEffect } from 'vue'
let stop = null
function startLater() {
setTimeout(() => {
stop = watchEffect(() => {
// 这是异步创建的 watcher
})
}, 1000)
}
onUnmounted(() => {
stop?.()
})
文章结论
watch 和 watchEffect 都是副作用工具,不是万能状态管理器。精确触发条件、请求取消、深度监听成本、回调执行时机和生命周期清理,决定了它们能否在真实项目里稳定工作。
本文没有在你的博客服务器或任何外部 API 中运行示例。代码中的公共测试接口仅用于解释官方示例结构。
来源
本文为中文技术解读,API 版本差异请以 Vue 官方文档为准。