Hi,我是前端人类学 !
Vue3 的响应式系统是一次彻底的重构------从 Object.defineProperty 换装为 Proxy,不仅解决了 Vue2 中新增属性无法响应、数组索引变更无法感知等痛点,更在性能和组织上做了全新设计。
本文将从源码层面拆解其核心机制,理解
ref与reactive的本质差异,并走通从依赖收集到派发更新的完整流程。
文章目录
-
- [一、为什么是 Proxy?------ Vue2 的痛点与 Vue3 的破局](#一、为什么是 Proxy?—— Vue2 的痛点与 Vue3 的破局)
- [二、reactive 源码拆解:Proxy 如何代理对象](#二、reactive 源码拆解:Proxy 如何代理对象)
-
- [2.1 类型校验与缓存](#2.1 类型校验与缓存)
- [2.2 可观察性校验](#2.2 可观察性校验)
- [2.3 创建 Proxy](#2.3 创建 Proxy)
- [2.4 baseHandlers 的核心:get 与 set](#2.4 baseHandlers 的核心:get 与 set)
- [三、ref 的本质:为原始值套上响应式外壳](#三、ref 的本质:为原始值套上响应式外壳)
-
- [3.1 RefImpl 类](#3.1 RefImpl 类)
- [3.2 模板自动解包](#3.2 模板自动解包)
- [3.3 ref vs reactive:如何选择](#3.3 ref vs reactive:如何选择)
- [四、依赖收集完整流程:track 与 trigger 的协作](#四、依赖收集完整流程:track 与 trigger 的协作)
-
- [4.1 activeEffect 与 effect 栈](#4.1 activeEffect 与 effect 栈)
- [4.2 track:依赖收集](#4.2 track:依赖收集)
- [4.3 trigger:派发更新](#4.3 trigger:派发更新)
- [4.4 Vue3.5 的响应式优化:双向链表](#4.4 Vue3.5 的响应式优化:双向链表)
一、为什么是 Proxy?------ Vue2 的痛点与 Vue3 的破局
在 Vue2 中,响应式数据通过 Object.defineProperty 对对象的已有属性逐个进行 getter/setter 劫持。这带来了两个核心限制:
- 无法检测属性的新增和删除 :
data初始化时不存在的属性,后续赋值无法触发响应式更新,必须使用Vue.set或this.$set来补救。 - 数组变更的局限性 :直接通过索引修改数组项
arr[0] = x无法被检测,Vue2 只能通过重写push、pop等 7 个数组方法来实现响应式。
Proxy 则完全不同------它代理的是整个对象 ,而非某个具体属性。任何对对象的操作(读取、写入、删除、in 操作等)都会先经过 Proxy 的拦截器。
javascript
const data = new Proxy(raw, {
get(target, key) { /* 拦截任意属性的读取 */ },
set(target, key, value) { /* 拦截任意属性的写入 */ }
})
这意味着无论属性是否预先存在,只要对代理对象进行操作,拦截器都会生效。新增属性 state.newProp = 'hi' 能自动触发更新,数组索引修改 arr[0] = 10 同样能被捕获。这是 Vue3 响应式系统最底层的进化。
二、reactive 源码拆解:Proxy 如何代理对象
Vue3 中 reactive API 的核心逻辑在 createReactiveObject 函数中。我们拆解其关键步骤:
2.1 类型校验与缓存
typescript
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 非对象直接返回
if (!isObject(target)) return target;
// 已经是代理对象,直接返回
if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
return target;
}
// 从缓存中取,避免重复代理
const existingProxy = proxyMap.get(target);
if (existingProxy) return existingProxy;
// ...
}
proxyMap 是一个 WeakMap,以原始对象为 key,代理对象为 value 进行缓存。这样同一个对象多次调用 reactive 会返回同一个代理实例。
2.2 可观察性校验
getTargetType 会检查目标对象的类型是否在白名单中:Object、Array、Map、Set、WeakMap、WeakSet 可以被代理;否则返回 TargetType.INVALID 直接跳过。
2.3 创建 Proxy
对于普通对象和数组,使用 baseHandlers(即 mutableHandlers);对于 Map/Set 等集合类型,使用 collectionHandlers。
typescript
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
);
proxyMap.set(target, proxy);
return proxy;
2.4 baseHandlers 的核心:get 与 set
mutableHandlers 是代理行为的核心,包含了 get、set、deleteProperty、has、ownKeys 等拦截器。
get 拦截器 负责依赖收集(track)和深度代理:
typescript
function get(target, key, receiver) {
// 内置标志位处理
if (key === ReactiveFlags.IS_REACTIVE) return true;
const res = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
// 深度响应式:取值时如果结果是对象,递归调用 reactive
return isObject(res) ? reactive(res) : res;
}
注意这里的惰性代理 :嵌套对象只在被访问到时才被代理,而非初始化时递归遍历所有层级,这带来了性能优势。
set 拦截器 负责触发更新(trigger):
typescript
function set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 值变化时触发更新
}
return result;
}
deleteProperty 同样会调用 trigger,确保删除属性也能触发响应。
三、ref 的本质:为原始值套上响应式外壳
reactive 只能代理对象,对于 number、string、boolean 等原始值无能为力。ref 正是为此而生------它通过一个包装对象让原始值也具备响应性。
3.1 RefImpl 类
ref 的底层是一个 RefImpl 类,核心结构非常简洁:
typescript
class RefImpl {
public __v_isRef = true;
private _value: any;
public dep: Set<ReactiveEffect> = new Set();
constructor(value) {
// 如果传入的是对象,内部用 reactive 转为响应式
this._value = isObject(value) ? reactive(value) : value;
}
get value() {
trackRefValue(this); // 依赖收集
return this._value;
}
set value(newVal) {
if (newVal !== this._value) {
this._value = isObject(newVal) ? reactive(newVal) : newVal;
triggerRefValue(this); // 触发更新
}
}
}
ref 返回的就是 RefImpl 的实例,通过 get value() 和 set value() 拦截对 .value 的读写,从而实现响应式。ref 的底层并不直接等于 reactive ------只有当传入的值是对象时,才会将对象交给 reactive 处理;对于原始值,ref 自己维护依赖收集和派发更新。
3.2 模板自动解包
在模板中直接使用 ref 时不需要写 .value,这是因为 Vue 在编译阶段对模板中的 ref 做了自动解包处理。另外,当 ref 作为 reactive 对象的属性被访问时,也会自动解包:
javascript
const count = ref(0);
const state = reactive({ count });
console.log(state.count); // 0,自动解包,不需要 .value
3.3 ref vs reactive:如何选择
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 任意类型(原始值 + 对象) | 仅对象(含数组、集合) |
| 访问方式 | .value |
直接访问属性 |
| 底层实现 | RefImpl 类 + .value 拦截 |
Proxy 代理整个对象 |
| 解构响应性 | 解构失去响应性 | 解构失去响应性 |
| 模板中使用 | 自动解包,无需 .value |
直接使用 |
使用建议 :对于基础类型和不确定类型的值,优先用 ref;对于结构清晰的对象状态,用 reactive 更自然。如果实在不确定,ref 是更通用的选择。
四、依赖收集完整流程:track 与 trigger 的协作
依赖收集是响应式系统的心脏。Vue3 的依赖管理以 targetMap 为核心数据结构,它是一个 WeakMap,结构为:
text
targetMap: WeakMap<target, Map<key, Set<ReactiveEffect>>>
即:目标对象 → (属性名 → 副作用函数集合)。
4.1 activeEffect 与 effect 栈
在依赖收集期间,需要一个全局变量记录当前正在执行的副作用函数 ------这就是 activeEffect。effect 函数(watchEffect、render 等底层都依赖它)在执行前会把自身设置为 activeEffect,执行完毕后重置。
typescript
class ReactiveEffect {
run() {
try {
effectStack.push((activeEffect = this));
return this.fn(); // 执行用户函数,触发 get 拦截
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
}
effectStack 用于处理嵌套 effect 场景(如组件嵌套),确保依赖始终收集到正确的副作用函数上。
4.2 track:依赖收集
当访问响应式数据的属性时,触发 get 拦截器,内部调用 track 函数:
typescript
function track(target, key) {
if (!activeEffect) return; // 没有活跃的 effect,跳过
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
dep.add(activeEffect);
}
track 做的事情很纯粹:建立 target → key → effect 的映射关系 。activeEffect 作为当前正在运行的副作用函数,被收入对应 key 的依赖集合中。
4.3 trigger:派发更新
当修改响应式数据的属性时,触发 set 拦截器,内部调用 trigger 函数:
typescript
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect.run()); // 遍历执行所有副作用函数
}
}
trigger 从 targetMap 中取出该属性对应的所有依赖(副作用函数),逐个执行,完成更新。
4.4 Vue3.5 的响应式优化:双向链表
Vue3.5 对响应式系统做了重大重构,引入了**双向链表(Link)**连接 Dep(依赖)和 Sub(订阅者),替代了之前 dep 和 effect 的直接关联。这一重构带来了 56% 的性能提升。
在新的模型中:
- Dep (响应式变量)和 Sub(副作用函数)之间不再直接引用。
- Link 作为桥梁,同时持有
dep和sub引用,并通过nextDep/prevDep、nextSub/prevSub形成 X 轴和 Y 轴的双向链表结构。
依赖触发时,从 Dep 指向的链表尾部开始,沿 Y 轴依次访问所有 Sub,执行对应的副作用函数。这种结构使得依赖的增删和遍历更加高效。
Vue3 的响应式系统可以概括为三层:
- 代理层 :
Proxy拦截对象操作,reactive负责对象响应式,RefImpl为原始值提供.value包装。- 依赖管理层 :
targetMap以target → key → Set(effect)的层级存储依赖,track收集,trigger派发。- 调度执行层 :
activeEffect和effectStack管理当前活跃的副作用函数,Vue3.5 进一步用双向链表优化了依赖的组织与遍历。理解这一套机制,既能帮你在日常开发中更自信地选择
ref还是reactive,也能让你在遇到响应式失效的"怪问题"时,快速定位根因------而不再需要靠"加个setTimeout试试"来碰运气。