一、前言
watch 已经很强了,但每次都要先想清楚"监听谁",依赖一多 watch([a, b, c, d], ...) 写到眼花。今天的主角 watchEffect 更懒更聪明:回调里用到谁,就自动监听谁,还天生自带立即执行。学完这篇,watch 三兄弟(computed / watch / watchEffect)就全部集齐了。
二、基础用法:自动依赖收集
watchEffect 接收一个函数,立即执行一次,执行过程中用到哪些响应式数据,就自动把哪些加入监听:
vue
<template>
<p>{{ count }} / {{ double }}</p>
<button @click="count++">+1</button>
</template>
<script setup>
import { ref, computed, watchEffect } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
watchEffect(() => {
console.log(`count=${count.value}, double=${double.value}`)
})
</script>
运行效果:
- 创建时立刻执行一次 (输出
count=0, double=0)------不需要 watch 的immediate: true - 点按钮改 count:自动重新执行------因为回调里"用到了" count
对比一下手动写法:
js
// watch 版:要手动声明监听 count
watch(count, () => {
console.log(`count=${count.value}, double=${double.value}`)
}, { immediate: true })
// watchEffect 版:用到谁听谁,依赖不用写
watchEffect(() => {
console.log(`count=${count.value}, double=${double.value}`)
})
依赖少没感觉,依赖五六个的时候,watchEffect 的省心程度直线上升。
三、watch 与 watchEffect 怎么选
不是替代关系,是分工关系,一张表看明白:
| 对比项 | watch | watchEffect |
|---|---|---|
| 依赖 | 手动显式声明 | 自动收集(用到谁听谁) |
| 首次执行 | 默认不执行,要 immediate |
✅ 创建即执行 |
| 新旧值 | ✅ 回调里有 (newVal, oldVal) | ❌ 没有 |
| 执行时机 | 数据变化后触发 | 创建即跑 + 依赖变化再跑 |
| 适合场景 | 需要知道"从几变到几"、按条件监听 | 依赖多个状态的副作用 |
选型口诀:
- 要旧值 / 要条件判断监听谁 → watch
- 一进来就要跑一次 + 依赖一堆状态 → watchEffect
比如"关键词变化后发请求"两者都能写,但"从上一页变成当前页时滚回顶部"(需要旧值)就必须 watch。
四、停止侦听与清理副作用
停止侦听:接收返回值,调用即停:
js
const stop = watchEffect(() => {
console.log('count 变了', count.value)
})
// 3 秒后不再监听
setTimeout(() => stop(), 3000)
写在 <script setup> 里时,组件卸载会自动停止,一般不用手动管。
清理副作用(onCleanup):每次重新执行前,先执行上一次注册的清理函数------经典场景是"取消上一次还没回来的请求",防止旧结果覆盖新结果:
js
watchEffect(async (onCleanup) => {
const kw = keyword.value
const controller = new AbortController()
onCleanup(() => controller.abort()) // 下一次执行前,掐断上一次请求
const res = await fetch('/api/search?kw=' + kw, { signal: controller.signal })
})
快速连续输入时,过期请求会被自动取消,只有最后一次的结果能落到页面------竞态问题一步到位。
五、综合实战:草稿自动保存
写博客最怕浏览器崩溃,用 watchEffect 做"边写边存":
vue
<template>
<input v-model="draft.title" placeholder="标题" />
<textarea v-model="draft.content" rows="8" placeholder="正文"></textarea>
<p>✅ 已自动保存 {{ savedCount }} 次</p>
<button @click="clear">清空</button>
</template>
<script setup>
import { reactive, ref, watchEffect } from 'vue'
const draft = reactive({
title: localStorage.getItem('draft_title') || '',
content: localStorage.getItem('draft_content') || ''
})
const savedCount = ref(0)
// 标题或正文任何一个变了,都自动存一次
watchEffect(() => {
localStorage.setItem('draft_title', draft.title)
localStorage.setItem('draft_content', draft.content)
savedCount.value++
})
function clear() {
draft.title = ''
draft.content = ''
}
</script>
三个细节品味一下:
- 初始化时立即保存了一次(watchEffect 天生立即执行)------正好完成"刷新页面恢复草稿"
- 回调里用了
draft.title和draft.content,两个都自动进了监听名单 ,不用手写watch([() => draft.title, () => draft.content], ...) - 刷新页面,草稿从 localStorage 自动恢复
【截图位置:输入内容后刷新页面,草稿自动恢复的运行效果】
六、踩坑记录
-
回调里"只想初始化跑一次"的逻辑会重复执行:watchEffect 依赖变了就重跑,初始化一次性逻辑(弹个提示、初始化地图)请放 onMounted(下一篇主角)
-
异步代码之后的依赖不会被收集:
jswatchEffect(async () => { const a = await getData() // await 之前的依赖能收集 console.log(a.value + count.value) // ❌ count 在 await 之后,不进监听名单 })依赖请尽量写在 await 之前,否则依赖变化不会重新触发。
-
不要在回调里修改自己依赖的数据 :
watchEffect(() => count.value++)直接死循环警告 -
需要新旧值时别硬用:watchEffect 没有 oldVal,硬要比较历史值就得自己存变量------不如直接用 watch
-
过度使用:回调里多写了一个无关的响应式数据,就会多一个触发源,"自动收集"是把双刃剑,注意别在回调里顺手用了不相干的状态
七、今日小结
- watchEffect:立即执行 + 用到谁自动听谁,专注副作用
- watch 有新旧值、显式依赖;watchEffect 自动收集、天生立即执行
stop()手动停止,onCleanup清理上一次副作用(防竞态)- 三兄弟分工:computed 算值、watch 精准监听、watchEffect 自动跑腿
作者:陈志宝
下篇预告
组件从"出生"到"销毁"有一整个人生,发请求要等"出生后"(onMounted),定时器要在"临终前"清掉(onBeforeUnmount)。下一篇生命周期钩子:在正确的时间做正确的事。