watchEffect 的原理
watchEffect 经常被一句话概括为“自动收集依赖的 watch”。这个说法有帮助,但还不够精确。
更准确的描述是:
watchEffect会立即执行一个副作用函数,并在执行过程中自动收集它同步访问到的响应式依赖;依赖变化后,再通过调度器重新运行这个副作用。
面试速答(30 秒版 TL;DR)
watchEffect的核心关键词:立即执行、自动收集依赖、副作用、cleanup、调度器。- 它和
watch的最大区别不是“能不能监听”,而是:watch:你明确指定监听源watchEffect:框架从副作用函数里自动推断依赖
- 它没有
oldValue - 异步
watchEffect只会追踪 第一次await之前 同步访问到的依赖
先记主链路:watchEffect(fn) -> 立即执行 -> 自动收集依赖 -> 依赖变化 -> 调度重跑。
1. 它为什么叫 Effect?
因为它的重点不是“返回一个值”,而是“执行一个副作用”。
典型副作用包括:
- 发请求
- 操作 DOM
- 写日志
- 注册/清理事件
例如:
watchEffect(() => {
console.log(count.value)
})
这里并不是为了得到某个派生值,而是“当依赖变化时,重新执行这段副作用逻辑”。
2. 原理核心:执行时自动收集依赖
内部可以简化理解为:
const runner = new ReactiveEffect(fn, scheduler)
runner.run()
当 fn 执行时:
- 当前 effect 会被设置为全局活跃 effect
- 函数里读取到的每个响应式属性,都会把这个 effect 收集进去
- 后续这些属性变化时,就会通知当前 effect 重新执行
所以 watchEffect 的依赖来源,不是你手写的 source,而是函数执行路径本身。
3. 为什么它是“立即执行”的?
因为 watchEffect 的设计目标就是:
- 先跑一次,把副作用建立起来
- 同时通过第一次运行完成依赖收集
这和默认懒执行的 watch 不同。
也正因为它首次就会执行,所以很适合写:
- 首次请求 + 依赖变化后重拉
- 首次订阅 + 依赖变化后重建订阅
4. cleanup 是如何配合工作的?
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log('tick')
}, 1000)
onCleanup(() => {
clearInterval(timer)
})
})
机制是:
- 首次运行 effect
- 注册 cleanup
- 依赖变化,要重新执行前
- 先执行上一次 cleanup
- 再运行新的 effect 逻辑
所以它很适合处理“旧副作用必须先清掉”的场景。
5. 为什么 watchEffect 没有 oldValue?
因为它不是“比较某个指定 source 的新旧值”,而是“重跑整个副作用函数”。
它关心的是:
- 有哪些依赖
- 依赖变了后要重新执行什么
而不是“我就要知道这个 source 的前后差异”。
如果你明确需要 newValue / oldValue,通常应该选 watch。
6. 异步场景为什么只追踪 await 前的依赖?
例如:
watchEffect(async () => {
console.log(userId.value)
await fetch('/api/user')
console.log(detail.value)
})
自动追踪通常只稳定覆盖第一次 await 之前同步读取到的依赖。
原因是:
- 依赖收集发生在 effect 的同步执行期
await之后已经切到后续异步恢复阶段- 此时不再属于同一次同步依赖收集窗口
所以异步 watchEffect 的经验法则是:
- 需要追踪的依赖,尽量在
await前读取
7. watchEffect 与 watch 的本质区别
| 维度 | watch | watchEffect |
|---|---|---|
| 首次执行 | 默认不执行回调 | 立即执行 |
| 依赖来源 | 手动指定 source | 自动收集 |
oldValue | 有 | 没有 |
| 精准控制 | 更强 | 更弱 |
| 写法成本 | 稍高 | 更省 |
一句话:
- 依赖源固定、需要精确控制,用
watch - 副作用依赖多、写 source 麻烦,用
watchEffect
8. 常见坑
8.1 以为它会自动追踪所有异步代码里的依赖
不会。尤其 await 之后读取的依赖,通常不在本轮自动收集的主窗口里。
8.2 依赖过多导致“看起来什么都能触发”
因为是自动收集,所以副作用函数里不小心读到很多响应式状态,就可能让触发范围变大。
8.3 把它当成 computed 用
watchEffect 是副作用,不是派生值缓存。要得到一个可复用的结果值,优先考虑 computed。
9. 面试高频答法
Q1:watchEffect 的原理是什么?
答:本质上是立即执行一个响应式 effect,执行期间自动收集同步访问到的依赖。依赖变化后,调度器会先执行上一次 cleanup,再重新运行这个 effect。
Q2:它和 watch 最大的区别是什么?
答:watch 由开发者明确指定监听源,能拿到 newValue / oldValue;watchEffect 不指定 source,而是自动从副作用函数里收集依赖,更省写法,但控制力更弱。
速记要点
watchEffect:立即执行 + 自动依赖收集- 本质是响应式 effect,不是值计算器
- 支持 cleanup
- 异步场景重点记:只稳定追踪首次
await前的同步依赖