在 Vue 的响应式系统里,Dep(Dependency,依赖)和 Watcher(观察者)是两个关键角色,它们之间形成了一种“发布-订阅”的对应关系,共同完成了“数据变化时自动通知视图更新”的核心流程。
它们分别是什么
- Dep:可以理解为“数据的通讯录”。每个被 Vue 转换成响应式的数据(如
data里的一个属性),背后都有一个对应的 Dep 实例。这个通讯录里记录了“哪些地方依赖了我”——也就是哪些 Watcher 订阅了这个数据的变化。 - Watcher:可以理解为“依赖数据的执行单元”。当你在模板里用了
{{ count }},或者写了一个watch回调,又或者定义了一个computed属性,Vue 内部都会创建一个对应的 Watcher。这个 Watcher 知道自己需要执行什么逻辑(更新 DOM、执行回调等),也知道自己依赖了哪些数据。
怎么建立关联:依赖收集
依赖收集的触发时机是在 Watcher 求值 的过程中。以 Vue 2 为例,流程如下:
- 当前 Watcher(比如组件的渲染 Watcher)开始工作,它把自己放到全局唯一的“当前目标”位置——
Dep.target。 - 这个 Watcher 在执行时,会去 读取 模板里用到的响应式数据,比如
this.count。 - 读取
count会触发它的getter(由Object.defineProperty或Proxy拦截)。在getter内部,count对应的 Dep 会执行dep.depend()。 dep.depend()看到此时Dep.target有值,就把这个 Watcher 加入自己的订阅者列表(subs)。于是,这个 Dep 就记住了一个“我的变化要通知这个 Watcher”。- 如果这个 Watcher 还读取了
this.name,那么name的 Dep 也会做同样的事,把当前 Watcher 加进去。
最终,一个 Watcher 可能订阅了多个 Dep,而一个 Dep 也可能被多个 Watcher 订阅——这是一种多对多的关系。渲染 Watcher 很可能订阅几十个 Dep,因为模板里用了几十个变量;而一个 Dep(比如全局用户信息)可能同时被渲染 Watcher、计算属性 Watcher、watch 回调 Watcher 共同订阅。
怎么触发更新:派发通知
当数据发生变化时,流程正好反过来:
- 你执行
this.count = 10,触发了count的setter。 setter内部调用dep.notify(),遍历自己通讯录(subs)里所有的 Watcher,逐个调用它们的update()方法。- 每个收到通知的 Watcher 并不会立即执行,而是先把自己放进一个更新队列,等待批量处理(这就是第 7 章要讲的异步更新与 nextTick 原理)。
- 在合适的时机,队列里的 Watcher 会重新求值。对于渲染 Watcher,就是触发组件重新渲染,生成新的虚拟 DOM 树,再 diff 出最小更新量应用到真实 DOM。
Vue 3 中的概念演进
Vue 3 用 effect 取代了“Watcher”这个命名,用 track 和 trigger 取代了 depend 和 notify,但核心关系模型完全一致:
- 你调用
ref()或reactive()创建的数据,内部仍然维护着一个依赖集合(类似 Dep)。 - 当
effect(或computed、watchEffect)执行时,其副作用函数被包裹,执行中读取响应式数据会触发track,将此 effect 与数据关联。 - 数据变化时调用
trigger,通知所有关联的 effect 重新运行。
一张图简单总结
[响应式数据 count] ── owns ──▶ Dep (subs: [Watcher A, Watcher B])
│
┌───────────────┼───────────────┐
▼ ▼
Watcher A (渲染组件) Watcher B (watch 回调)
依赖: count, name 依赖: count
这种一对多的订阅模型让 Vue 实现了精确更新——count 改变时,只有真正用到它的 Watcher 才会被通知,而不会波及到无关区域。这也是响应式系统高效运转的基石。