一句话摘要
Vue 3 的响应式核心,是通过 Proxy 拦截读写操作,再借助 effect、track、trigger 和依赖映射表,把数据读取和副作用函数重新执行关联起来。
目录
核心数据结构
三个表:targetMap(WeakMap)、depsMap(Map)、dep(Set)。
Vue 内部维护了一个全局的弱引用映射字典(WeakMap),数据结构大概是:
target(被代理对象) -> key(对象属性) -> Set<effect>对应关系如下:
targetMap:key是对象,value是属性表。depsMap:key是属性,value是effect表。dep:value是effect。
核心函数
三个函数:effect、track、trigger。
1. effect:副作用函数
effect 接收一个函数 fn。当你调用 effect(fn) 时:
- 它会立即执行一次
fn。 - 在执行
fn的过程中,如果读取了响应式数据(比如reactive生成的Proxy对象属性),就会触发该属性的读取拦截器(getter)。 - 建立起被访问数据与这个
fn之间的联系,以便将来数据变化时,这个fn能够被再次执行。
2. track:依赖收集
track 一般在响应式对象(Proxy)的 get(读取)操作拦截器中被内部调用。
其核心作用是把当前正在运行的 effect 记录下来。
3. trigger:派发更新
trigger 一般在响应式对象(Proxy)的 set(修改)操作拦截器中被内部调用。
其核心作用是取出对应的 effect 并执行。
响应式更新流程
effect(fn) -> 执行 fn()读取数据触发 Proxy getter -> 调用 track 收集 activeEffect修改数据触发 Proxy setter -> 调用 trigger 取出 effect -> 重新执行 effect(fn)如何拦截 set 和 get
- Vue 2:ES5
Object.defineProperty() - Vue 3:ES6
Proxy
反射和代理
代理(Proxy)是一个对象的占位符,默认情况下对该对象进行委托。

Proxy() 的第二个参数叫处理函数(Handler),可以传递一个诱捕器(trap),用于拦截各种基础操作,比如属性查找、枚举,我们就可以用来拦截 get 和 set 了。
function reactive(target) { const handler = { get(target, key, receiver) { console.log('触发了 Get,key = ' + key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { console.log('触发了 Set,key = ' + key + ',value = ' + value); return Reflect.set(target, key, value, receiver); } };
// 返回一个代理对象,我们可以像使用原对象一样使用它。 return new Proxy(target, handler);}
let product = reactive({ price: 5, quantity: 2 });注意:receiver(接收器)的作用是保证当我的对象有继承自其他对象的值或函数时,this 指针能正确指向使用的对象。
加入 track 和 trigger
再加入 track 和 trigger:
function reactive(target) { const handler = { get(target, key, receiver) { let result = Reflect.get(target, key, receiver); track(target, key); // 收集依赖 return result; }, set(target, key, value, receiver) { let oldValue = target[key]; let result = Reflect.set(target, key, value, receiver); // 只有当值发生改变时才触发更新 if (oldValue !== value) { trigger(target, key); // 派发更新 } return result; } };
return new Proxy(target, handler);}
let product = reactive({ price: 5, quantity: 2 });然后我们尝试创建一个对象,并且使用上面的 reactive 方法:
let product = reactive({ price: 5, quantity: 2 });let total = 0;
let effect = () => { total = product.price * product.quantity;};
effect(); // 初始执行,触发 getter 依赖收集console.log(total); // 输出 10
product.quantity = 3; // 触发 setter 派发更新console.log(total); // 输出 15详细运行过程
1. 建立响应式对象
我们通过 reactive({ price: 5, quantity: 2 }) 创建对象,它返回了一个 Proxy 代理对象 product。此时 product 的读取(get)和修改(set)操作都已经分别被拦截处理。
2. 初次执行 effect 并收集依赖(track)
手动调用执行 effect() 时,它内部读取了 product.price 和 product.quantity。此时触发了代理对象 product 的 get 拦截器。
在 get 拦截器中,内部调用了 track(target, key):
track把当前的effect副作用函数收集到全局依赖字典(targetMap->depsMap->dep)中,分别与price和quantity这被访问的属性建立联系。total计算结果为5 * 2 = 10。
3. 修改数据并自动派发更新(trigger)
当代码走到 product.quantity = 3 时,是对 product 属性赋值,此操作触发了代理对象的 set 操作拦截器。
在 set 拦截器中:
- 我们传入的新值
3(value)和原来的值2(oldValue)不相等,即判定oldValue !== value成立。 - 由于值发生了变化,拦截器继续调用
trigger(target, 'quantity')发出更新派发信号。 trigger去全局依赖字典找到订阅在当前对象'quantity'属性上的所有effect函数集合并执行它们。- 这样我们原本的
effect函数就会重新隐式运行一遍(total = 5 * 3),从而使total更新为15。 - 所以最后一句
console.log(total)时,得到的是最新的结果15。
本章小结
这个例子完整串起了初始化执行、读取收集、赋值触发和副作用重跑四个环节。最终 total 从 10 更新为 15,正是 trigger 派发更新后的结果。
第三步的延伸:如果在外部再加入一个独立的 get 操作,会有什么问题吗?
假如我们在最下面直接加上一句:
console.log('Updated quantity to = ' + product.quantity);答案是:有可能会导致错误收集。
当你执行 product.quantity 这一读取操作时,自然会再次触发 Proxy 代理对象的 get 拦截器。而在 get 拦截器内,会不可避免地自动执行 track(target, key) 去收集依赖。
问题在于这里所处的上下文:这段 console.log 代码是在全局下直接运行的,它并没有被包裹在任何副作用 effect 函数内。
如果没有进行安全判断:我们的 track 函数可能会去收集那个根本不存在的依赖(可能是 undefined,也可能是上次残留还没被清除的旧 effect),将一个无效的依赖强行塞进我们的 dep 集合里,这会导致内存泄漏,甚至是调用报错。
这就引出了真正完善的依赖收集机制里非常关键的一环:在真正的 Vue 3 源码中,内部会维护一个全局变量(通常命名为 activeEffect)来时刻保存当前正在执行的副作用函数。在进入 effect 执行时赋值给它,出 effect 时清空。
而 track 在尝试收集依赖之前,做的第一件事就是检查 activeEffect 是否存在。只有存在才会把它添加进弱映射字典,以防止脱离副作用的“越界收集”。
本章小结
依赖收集必须建立在当前确实存在 activeEffect 的前提下。脱离副作用函数的读取操作不能被随意收集,否则可能产生无效依赖、内存泄漏或调用报错。
ref 的实现
对于基本数据类型(如字符串、数字等),由于它们不是对象,我们无法给它们应用 Proxy 代理。因此,Vue 的方案是用一个对象包裹它,这就是 ref。
ref 的核心是利用了对象访问器(Getter / Setter 计算属性)来获取或设置 .value 的值:
function ref(raw) { const r = { get value() { track(r, 'value'); // 收集依赖 return raw; }, set value(newVal) { if (raw !== newVal) { raw = newVal; trigger(r, 'value'); // 派发更新 } } }; return r;}computed 的实现
function computed(getter) { let result = ref();
// 每次 getter 里的依赖数据变化时,effect 都会重新执行,给 result.value 赋予新计算的值 effect(() => { result.value = getter(); });
return result;}
let product = reactive({ price: 5, quantity: 2 });
let salePrice = computed(() => { return product.price * 0.9;});
let total = computed(() => { return salePrice.value * product.quantity;});
console.log(total.value); // 此时由于依赖响应式数据改变,总价会自动重算注意:源码实际上的处理方式(基于 ComputedRefImpl)是真正的 computed 是一个带有懒计算标志位(_dirty)的对象。
依赖发生改变时,它并不立即运算求新值,而是通过传入 scheduler 调度器把 _dirty 标记为 true(脏数据),这相当于告诉 Vue:“我的值已经过期了”。
只有当你再次读取 salePrice.value 触发访问时,Vue 发现 _dirty 为 true,才会重新调用 getter() 执行真正的计算,算出新值后存起来并将 _dirty 置为 false。
这期间如果没有依赖发生变更(_dirty 依然是 false),它会一直直接返回上次计算缓存的值,绝不重复运算。
本章小结
简化版 computed 可以用 ref 和 effect 拼出来;源码里的 computed 还会通过 _dirty 和 scheduler 做懒计算与缓存,避免每次读取都重复执行 getter()。
总结
Vue 3 响应式系统可以按一条主线理解:用 Proxy 拦截对象的读取和修改,在读取时通过 track 收集当前 activeEffect,在修改时通过 trigger 找到并重新执行对应的副作用函数。
ref 解决的是基本数据类型不能直接被 Proxy 代理的问题;computed 则是在响应式依赖之上加入计算、缓存和懒更新机制。理解这几层关系后,再看 reactive、ref、computed 的行为,就能更容易串起 Vue 3 响应式的整体流程。
部分信息可能已经过时




