响应式系统的核心只有两件事:知道谁在用这个数据(依赖收集),以及数据变了通知谁(派发更新)。Vue 3 和 Vue 2 的具体实现不同,但思想相通。下面以 Vue 3 为主讲解全链路,再简要对比 Vue 2。
一、Vue 3 的核心角色:effect、track、trigger
1. effect:副作用函数
任何一个依赖了响应式数据的函数,都可以被包装成一个 effect。比如组件的渲染函数、watch 的回调、computed 的 getter。effect 执行时会自动收集它触碰到的所有响应式数据。
effect(() => {
document.body.innerText = state.count
})
2. track:依赖收集
当 effect 执行并读取了某个响应式属性(如 state.count)时,Vue 内部的 track 函数会被触发。它的任务是把“这个 effect”和“这个属性”绑定起来。
存储结构大致是这样的(简化版):
targetMap: WeakMap<源对象, Map<属性, Set<effect>>>
- 第一层:用
WeakMap以响应式对象为 key,确保对象被垃圾回收时对应依赖也消失。 - 第二层:每个对象的每个属性(或键)对应一个
Map,key 是属性名。 - 第三层:属性值对应一个
Set,里面存放所有依赖该属性的effect。
所以当 effect 读了 state.count,track 就把它加入 targetMap.get(state).get('count') 这个 Set 里。
3. trigger:派发更新
当 state.count 被修改时,trigger 会沿着同样的路径找到 targetMap 里存的 effect 集合,然后执行(或重新调度)它们。因为响应式数据通过 Proxy 拦截了 set 操作,trigger 就是在 set 拦截中调用的。
整条链路:
effect 执行 → 读数据 → Proxy get 拦截 → track 收集依赖
↓
数据修改 → Proxy set 拦截 → trigger 找到依赖的 effect 集合 → 执行 effect
二、一个更具体的示例
const obj = reactive({ a: 1, b: 2 })
effect(() => {
console.log(obj.a)
})
// 打印 1
// 此时 targetMap: { obj → { a → [ effect ] } }
obj.a = 10 // trigger 触发,effect 再次执行,打印 10
obj.b = 20 // 没有 effect 依赖 b,什么也不发生
组件渲染也是同样的道理。每个组件的渲染函数本身就是一个 effect。当组件模板里用到了 state.count,该渲染 effect 就被收集为 count 的依赖。count 一变,组件自动重渲染。
三、依赖清理与避免重复收集
一个 effect 可能被执行多次,每次执行时依赖的数据可能不同。比如:
effect(() => {
if (state.flag) {
console.log(state.a)
} else {
console.log(state.b)
}
})
- 第一次执行,
flag === true,依赖了flag和a。 - 当
flag变为false重新执行,这次依赖了flag和b,不再依赖a。
如果不做清理,a 的依赖集合里仍然保留着这个 effect,以后 a 变化时还会触发它执行,但实际上这时 a 已经不再影响渲染结果,白白浪费性能。
Vue 3 解决了这个问题:每个 effect 都会记录自己当前依赖了哪些属性,在重新执行之前,会先清除自己与这些属性的绑定,然后执行时重新收集。这样就能保证依赖集合始终是最新的。
具体实现:
- 每个
effect上有一个deps数组,存放着所有它订阅过的属性对应的Set(或Dep)。 - 每次
effect执行前,先遍历deps,把自己从这些Set中删除。 - 执行过程中,
track又会把新的依赖加进来。 - 这种“先清后收”的流程,确保了永远不会有多余的旧依赖残留。
同时也避免了重复收集:同一个属性被多次读取时(比如模板里用了两次 state.count),track 内部会检查 Set 是否已经包含当前 effect,如果是就跳过,所以 Set 里每个 effect 只出现一次。
四、Vue 2 的对应机制:Dep 与 Watcher
Vue 2 的角色名字不同,但结构类似:
- Dep 相当于一个属性的依赖收集容器(类似前面 Map 里的 Set)。
- Watcher 相当于
effect。每个组件渲染、每个watch、每个computed都对应一个Watcher实例。
Vue 2 也是通过 Object.defineProperty 的 getter 里调用 dep.depend() 收集 Watcher,setter 里调用 dep.notify() 通知所有 Watcher 更新。依赖清理也是通过 Watcher 自身的 deps 数组实现,机制一致。
五、为什么你需要理解这条链路?
- 避免响应式失效:只有当数据在
effect执行期间被同步读取,才会被收集。如果你在setTimeout或异步回调里读取数据,或者使用普通函数的返回值去引用,可能收集不上,导致数据变了视图不更新。 - 理解
nextTick的必要性:多个数据变更在同一个同步块内,Vue 会把 effect 加入微任务队列批量执行,避免多次不必要的计算和 DOM 操作。 - 防止内存泄漏:如果手动创建 effect(如使用
watchEffect),在组件卸载时应当停止,否则 effect 会一直持有组件数据的引用,导致组件无法被垃圾回收。Vue 3 的effectScope可以帮助统一管理。 - 优化
computed和watch的性能:知道依赖是精确收集的,你就可以放心拆分计算属性和侦听器,而不用担心性能浪费。
简单总结:数据变了知道通知谁,是因为中间藏着一张精确的“依赖地图”;视图更新为什么高效,是因为这张地图只指向真正需要更新的地方。 这就是响应式系统最核心的“后勤保障”。