依赖收集与派发更新机制
说明 Vue 3 中 track / trigger 与 targetMap 的工作方式。前置:v2-v3 差异。
一、依赖收集(Track)
依赖收集的目的是建立 数据 → 副作用函数(effect) 的映射关系,使得当数据变化时,能够自动执行相关的副作用(如组件渲染、计算属性、watch)。
1. 触发时机:在 getter 拦截中
Vue 3 使用 Proxy 拦截对象属性的读取操作(get 陷阱)。当组件渲染、computed 或 watch 运行时,会读取响应式数据,从而触发 get 拦截器,track 函数被调用。
// 简化逻辑
function createGetter() {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
// 如果是对象,递归代理(懒代理)
if (isObject(res)) return reactive(res);
return res;
};
}
2. 映射关系的存储结构:targetMap
Vue 3 维护了一个全局的 WeakMap,名为 targetMap。其结构为:
- 键:原始对象(
target) - 值:一个
Map,键为属性名(key),值为一个Set集合,存放依赖该属性的所有副作用函数(effect)。
targetMap (WeakMap)
├─ target1 (obj) → Map
│ ├─ key1 → Set[effect1, effect2, ...]
│ └─ key2 → Set[effect3, ...]
└─ target2 → Map...
3. 当前激活的副作用:activeEffect
为了知道当前正在执行的副作用是哪一个,Vue 3 使用了一个全局变量 activeEffect。当执行 effect 函数时,会先将自身赋值给 activeEffect,然后执行回调(回调中读取数据,触发 track),最后重置。
function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run(); // 首次执行,触发依赖收集
}
class ReactiveEffect {
constructor(fn) {
this.fn = fn;
}
run() {
activeEffect = this;
return this.fn();
}
}
在 track 中,如果存在 activeEffect,就将它添加到对应属性的依赖集合中。
二、派发更新(Trigger)
当响应式数据被修改时(触发 set 拦截器),Vue 3 会找出依赖该数据的所有副作用函数,并重新执行它们。
1. 触发时机:在 setter 拦截中
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 如果值真的变化了,触发更新
if (oldValue !== value) {
trigger(target, key, value, oldValue);
}
return result;
};
}
2. 异步更新队列与 nextTick
Vue 3 并不会同步立即执行每个副作用,而是将需要执行的副作用缓冲到一个队列中,然后通过微任务(Promise.then 或 queueMicrotask)在下一轮事件循环统一执行。这称为 批量异步更新。
- 相同的副作用只会被加入队列一次(去重)。
- 通过
nextTick可以在 DOM 更新后执行回调。 - 底层实现:
queueFlush函数将flushJobs放入微任务队列。
let isFlushPending = false;
const pendingPreFlushCbs = [];
function queueJob(job) {
if (!pendingPreFlushCbs.includes(job)) {
pendingPreFlushCbs.push(job);
if (!isFlushPending) {
isFlushPending = true;
Promise.resolve().then(flushJobs);
}
}
}
flushJobs 会将队列中的所有副作用按顺序执行,完成之后再清空。
3. trigger 的查找过程
trigger 根据被修改的 target 和 key,从 targetMap 中取出对应的依赖集合,遍历执行每个 effect。如果 effect 配置了 scheduler(调度器),则会调用调度器(通常是放入队列)。
三、ref 与 reactive 的底层实现差异
1. reactive
- 适用对象:仅能代理对象类型(Object、Array、Map、Set 等)。
- 实现:直接基于
Proxy包装原对象,返回一个代理对象。代理对象和原对象保持引用关系,所有属性访问都被拦截。 - 缺陷:不能直接处理原始值(
number、string、boolean等),因为Proxy无法代理原始值。
2. ref
- 适用对象:可以包装任何类型,尤其是原始值。
- 实现:创建一个包含
value属性的普通对象,并为该属性添加响应式逻辑。- 如果传入的是对象,内部会调用
reactive将该对象转换为响应式代理。 - 如果传入的是原始值,则通过包装对象
.value的 getter/setter 实现响应式(内部仍使用类似trackRefValue/triggerRefValue的独立依赖管理)。
- 如果传入的是对象,内部会调用
- 访问方式:必须通过
.value读取或修改(模板中自动解包,但脚本中需要.value)。
简化版 ref 实现
function ref(value) {
const r = {
_value: value,
get value() {
track(r, "value"); // 依赖收集
return this._value;
},
set value(newVal) {
if (newVal !== this._value) {
this._value = newVal;
trigger(r, "value"); // 派发更新
}
},
};
if (isObject(value)) r._value = reactive(value);
return r;
}
3. 核心差异总结
| 特性 | reactive | ref |
|---|---|---|
| 输入类型 | 仅对象(包括数组、集合) | 任意类型(原始值 + 对象) |
| 底层代理 | Proxy | 普通对象 + getter/setter(对象时内嵌 reactive) |
| 访问方式 | 直接访问属性 | 通过 .value 访问 |
| 解构响应性 | 会丢失,需 toRefs | 直接解构 .value 也会丢失,一般不解构 |
| 模板中的行为 | 直接使用 | 自动解包(直接写变量名,无需 .value) |
| 典型使用场景 | 复杂状态对象、表单数据 | 单个原始值、需要重新赋值的场景 |
四、工作流示例:组件渲染
- 组件初始化:创建
setup()中定义的响应式数据(ref/reactive)。 - 首次渲染:执行
render函数(副作用),读取响应式数据 → 触发getter→track收集当前渲染副作用作为依赖。 - 数据变更:用户交互或异步操作修改数据 → 触发
setter→trigger将渲染副作用放入异步队列。 - 微任务执行:
flushJobs执行队列,重新调用render函数,更新虚拟 DOM 并 patch 到真实 DOM。 nextTick回调在 DOM 更新后执行。
五、性能相关的设计考量
- 懒递归:
reactive只在读取嵌套对象时才进行深层代理,避免初始化开销。 - 依赖清理:每次重新执行副作用前,会清除旧依赖,避免无效依赖积累(通过
effect的deps数组)。 - 调度器:允许自定义更新时机(如
watch的flush: 'sync'或'post')。 - 异步队列去重:同一个副作用多次触发只会执行一次。
理解这些机制,有助于写出更高性能的 Vue 应用,也能在遇到响应式相关 BUG(如数据改变但视图未更新)时快速定位问题。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| Vue:深入响应式 | 官方说明 |
| Vue:ref | ref API |
| Vue:reactive | reactive API |
相关文章
响应式进阶
toRefs、shallowRef、readonly、customRef 等 API 的适用场景与注意点。前置:依赖收集。
Vue 2 与 Vue 3 响应式差异
Vue 2 与 Vue 3 响应式实现的核心差异如下。基础概念见 Vue 基础。
资源优化
图片懒加载、按需引入与 Web Vitals 监控。见 HTML 图片、CSS 字体优化。
状态管理:Pinia 与 Vuex
Pinia 是 Vue 官方推荐的状态管理库(Vue 3 新项目默认选型)。Vuex 4 仍可与 Vue 3 配合,用于维护旧项目。见 Vue 基础。
Vue 生态关键库深度集成
VueUse、Vite 与 Nuxt 是 Vue 3 生态中常用的效率与工程化方案。概览见 Vue 基础。
测试策略
Vue 应用常用 Vitest + @vue/test-utils 做单元/组件测试,Playwright 或 Cypress 做 E2E。见 Vue 基础。
Series
reactivity
2 / 3