深入理解 Vue 3 响应式系统原理:Proxy、Track 与 Trigger 的协奏曲
Vue 3 的响应式系统从 Vue 2 的 Object.defineProperty
升级为基于 Proxy
的全新实现,带来了更强的性能和更高的灵活性。本文将深入剖析其底层原理,解构响应式的关键组成部分,并辅以代码和图示,带你看懂 Vue 3 的响应式魔法。
一、引言:从 Vue 2 到 Vue 3 的响应式变革
Vue 2 使用 Object.defineProperty
拦截对象属性读写,但存在如下限制:
- 不能监听数组索引和
length
的变化; - 不能动态添加属性;
- 深层嵌套对象递归成本高。
Vue 3 使用 Proxy
解决了这些痛点,实现了更强大、灵活的响应式系统。
二、Vue 3 响应式系统架构图
graph TD
A[reactive包裹的对象] --> B[Proxy 对象]
B --> C[getter -> track 收集依赖]
B --> D[setter -> trigger 派发更新]
C --> E[activeEffect 注册副作用]
D --> F[effect 重新执行 -> 更新视图]
三、Proxy 是如何劫持对象的?Reflect 有何作用?
Vue 3 利用 Proxy
对原始对象进行拦截,监听属性访问和修改操作:
ts
const handler = {
get(target, key, receiver) {
// 依赖收集
track(target, key);
const result = Reflect.get(target, key, receiver);
return typeof result === 'object' ? reactive(result) : result;
},
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;
}
};
function reactive(target) {
return new Proxy(target, handler);
}
Proxy 拦截职责:
get
:用于依赖收集(track)set
:用于触发依赖(trigger)
Reflect 的作用:
- 提供更规范的原始操作执行方式
- 返回布尔值表示操作是否成功
- 避免直接访问原对象,支持继承关系和代理转发
四、track 与 trigger:响应式的灵魂
Vue 响应式系统核心在于:依赖收集(track) 与 依赖触发(trigger) 。
1. 依赖收集 track
ts
let activeEffect = null;
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);
}
每次读取属性时,当前副作用函数(effect)会作为依赖被存储在 targetMap
中,数据结构如下:
ts
// WeakMap<target, Map<key, Set<effects>>>
targetMap = {
obj: {
name: Set(effect1, effect2),
age: Set(effect3)
}
}
2. 依赖触发 trigger
ts
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
deps.forEach(effect => effect());
}
}
当属性变化时,所有依赖这个属性的 effect 函数将被重新执行。
五、effect 是什么?响应式如何与副作用函数关联?
effect 是用来注册副作用函数的机制。它将副作用函数与响应式数据绑定起来:
ts
function effect(fn) {
const run = () => {
activeEffect = run;
fn();
activeEffect = null;
};
run();
}
例如:
ts
const state = reactive({ count: 0 });
effect(() => {
console.log(`count changed: ${state.count}`);
});
上述代码中,state.count
被读取,因此 console.log
所在的 effect 被收集。当 count
改变时,effect 会重新执行,自动更新输出。
六、如何避免重复收集?如何处理嵌套对象?
1. 避免重复收集
使用 Set
去重,保证同一个 effect 只被收集一次:
ts
deps.add(activeEffect); // Set 会自动去重
2. 嵌套对象自动递归响应式
在 get
中递归调用 reactive()
:
ts
const result = Reflect.get(target, key, receiver);
return typeof result === 'object' && result !== null
? reactive(result)
: result;
这确保即使嵌套对象尚未访问,也能在访问时被转为响应式。
例如:
ts
const state = reactive({
user: {
name: 'Heart',
address: {
city: 'guangzhou'
}
}
});
只有当你访问 state.user.address.city
时,对应层级才会被递归地变成 Proxy。
七、小结:Vue 响应式的设计与优势
特性 | Vue 2 | Vue 3 |
---|---|---|
实现方式 | Object.defineProperty | Proxy |
动态属性 | 不支持 | 支持 |
数组监听 | 局限较多 | 完善支持 |
嵌套对象处理 | 初始化递归 | 访问时递归,懒代理 |
性能 | 响应式递归开销大 | 更加高效、灵活 |
Vue 3 的响应式机制不仅解决了 Vue 2 的痛点,还借助现代 JavaScript 特性构建了一个轻量而强大的响应式系统,是前端响应式范式演进的重要一环。