跳到主要内容

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)
})
})

机制是:

  1. 首次运行 effect
  2. 注册 cleanup
  3. 依赖变化,要重新执行前
  4. 先执行上一次 cleanup
  5. 再运行新的 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. watchEffectwatch 的本质区别

维度watchwatchEffect
首次执行默认不执行回调立即执行
依赖来源手动指定 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 / oldValuewatchEffect 不指定 source,而是自动从副作用函数里收集依赖,更省写法,但控制力更弱。

速记要点

  • watchEffect:立即执行 + 自动依赖收集
  • 本质是响应式 effect,不是值计算器
  • 支持 cleanup
  • 异步场景重点记:只稳定追踪首次 await 前的同步依赖