深入理解Proxy的Trap机制:Vue如何拦截get/set/deleteProperty实现深度依赖收集
大家好,今天我们来深入探讨Vue.js中响应式系统的核心机制之一:Proxy的Trap。Vue 3 使用 Proxy 替代了 Vue 2 中的 Object.defineProperty,带来了性能和功能上的提升。理解 Proxy 的 Trap 机制,对于我们理解 Vue 的响应式原理至关重要。
什么是 Proxy 和 Trap?
Proxy 是 ES6 引入的一个强大的特性,它允许你创建一个对象的代理,并拦截对该对象的基本操作。你可以理解为在目标对象前面设置了一层“拦截器”,所有对目标对象的操作都会先经过这个代理。
而 Trap (也称为 handler) 是 Proxy 的核心概念。Trap 是一系列函数,定义了在代理对象上执行特定操作时应该调用的行为。换句话说,Trap 定义了代理对象如何响应各种操作。
常见的 Trap 包括:
| Trap | 拦截的操作 |
|---|---|
get |
读取属性值 |
set |
设置属性值 |
deleteProperty |
删除属性 |
has |
使用 in 操作符判断属性是否存在 |
ownKeys |
使用 Object.getOwnPropertyNames 或 Object.getOwnPropertySymbols 获取对象的所有自身属性键 |
apply |
调用函数 |
construct |
使用 new 操作符调用构造函数 |
Vue.js 主要利用了 get、set 和 deleteProperty 这三个 Trap 来实现依赖收集和更新通知。
Vue 2 vs Vue 3: Object.defineProperty vs Proxy
在深入了解 Vue 3 如何使用 Proxy 之前,我们先回顾一下 Vue 2 如何使用 Object.defineProperty 实现响应式。
Vue 2 的 Object.defineProperty:
Vue 2 使用 Object.defineProperty 来劫持对象的属性,从而实现响应式。它通过 getter 和 setter 函数来监听属性的读取和修改。
function defineReactive(obj, key, val) {
// 递归处理嵌套对象
if (typeof val === 'object' && val !== null) {
val = observe(val); // 递归观测
}
let dep = new Dep(); // 每个属性创建一个 Dep 实例
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 依赖收集
if (Dep.target) {
dep.depend(); // 将当前 watcher 添加到 dep 的 subscribers 中
}
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify(); // 通知所有订阅者
}
});
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
return obj;
}
class Dep {
constructor() {
this.subs = [];
}
depend() {
if (Dep.target && !this.subs.includes(Dep.target)) {
this.subs.push(Dep.target);
}
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
Dep.target = null; // 当前正在执行的 watcher
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.expOrFn = expOrFn;
this.cb = cb;
this.value = this.get();
}
get() {
Dep.target = this; // 设置当前 watcher
const value = this.vm[this.expOrFn]; // 触发 getter,进行依赖收集
Dep.target = null; // 清空当前 watcher
return value;
}
update() {
const newValue = this.vm[this.expOrFn];
if (newValue !== this.value) {
this.cb.call(this.vm, newValue, this.value);
this.value = newValue;
}
}
}
// 示例
const vm = {
message: 'Hello'
};
observe(vm);
new Watcher(vm, 'message', (newValue, oldValue) => {
console.log(`message changed from ${oldValue} to ${newValue}`);
});
vm.message = 'World'; // 输出: message changed from Hello to World
缺点:
- 只能劫持已存在的属性: 需要提前知道对象的所有属性才能进行劫持,对于动态添加的属性无法监听。
- 需要遍历对象的所有属性: 对于大型对象,性能开销较大。
- 无法监听数组的变化: 需要重写数组的一些方法 (
push,pop,shift,unshift,splice,sort,reverse) 才能监听到数组的变化。 - 深度监听需要递归遍历: 深度监听需要递归遍历对象的每一个属性,增加了开销。
Vue 3 的 Proxy:
Vue 3 使用 Proxy 解决了 Object.defineProperty 的一些问题。
- 可以劫持整个对象: 不需要提前知道对象的所有属性,可以监听动态添加的属性。
- 性能更好: Proxy 的性能通常比
Object.defineProperty更好,尤其是在大型对象上。 - 可以监听数组的变化: 不需要重写数组的方法,可以直接监听数组的变化。
- 深度监听更加简洁: 深度监听可以通过递归地创建 Proxy 来实现,不需要显式地遍历对象。
Vue 3 如何使用 Proxy 实现响应式
下面我们来看一个简化的 Vue 3 响应式系统的示例,展示如何使用 Proxy 的 get、set 和 deleteProperty Trap 来实现依赖收集和更新通知。
const isObject = (val) => val !== null && typeof val === 'object';
function reactive(target) {
if (!isObject(target)) {
return target;
}
const existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 1. 依赖收集
track(target, key);
const res = Reflect.get(target, key, receiver);
if (isObject(res)) {
return reactive(res); // 递归处理嵌套对象
}
return res;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 2. 触发更新
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
const hasKey = Reflect.has(target, key);
const result = Reflect.deleteProperty(target, key);
if (hasKey && result) {
// 3. 触发更新
trigger(target, key);
}
return result;
}
});
reactiveMap.set(target, proxy);
return proxy;
}
const reactiveMap = new WeakMap();
let activeEffect = null;
function effect(fn) {
const effectFn = () => {
try {
activeEffect = effectFn;
return fn(); // 执行 fn,触发依赖收集
} finally {
activeEffect = null;
}
};
effectFn.deps = []; // 存储当前 effect 依赖的 dep 集合
effectFn();
return effectFn;
}
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
trackEffects(dep);
}
function trackEffects(dep) {
if (!activeEffect) return;
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep); // 用于 cleanupEffect
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
triggerEffects(dep);
}
function triggerEffects(dep) {
const effectsToRun = new Set(dep); // 防止无限循环
effectsToRun.forEach(effectFn => effectFn());
}
// 示例
const data = {
name: 'John',
age: 30,
address: {
city: 'New York'
},
hobbies: ['reading', 'coding']
};
const state = reactive(data);
effect(() => {
console.log(`Name: ${state.name}, Age: ${state.age}, City: ${state.address.city}, Hobbies: ${state.hobbies.join(', ')}`);
});
state.name = 'Jane'; // 输出: Name: Jane, Age: 30, City: New York, Hobbies: reading, coding
state.address.city = 'Los Angeles'; // 输出: Name: Jane, Age: 30, City: Los Angeles, Hobbies: reading, coding
state.hobbies.push('music'); // 输出: Name: Jane, Age: 30, City: Los Angeles, Hobbies: reading, coding, music
delete state.age; // 不会触发更新,因为没有监听 age 属性的删除
console.log(state.age) // undefined, 但删除操作已经生效
代码解释:
-
reactive(target): 接收一个对象作为参数,返回该对象的响应式代理。- 如果传入的不是对象,则直接返回。
- 使用
WeakMap(reactiveMap) 缓存已经创建的 Proxy,避免重复代理同一个对象。 - 创建
Proxy对象,并定义get、set和deletePropertyTrap。 - 将原始对象和代理对象存储在
reactiveMap中。
-
get(target, key, receiver): 拦截属性读取操作。- 依赖收集 (
track(target, key)): 当读取属性时,会调用track函数,将当前激活的effect函数(如果有)添加到该属性的依赖集合中。 - 递归代理 (
reactive(res)): 如果读取的属性值是对象,则递归调用reactive函数,将其转换为响应式对象。
- 依赖收集 (
-
set(target, key, value, receiver): 拦截属性设置操作。- 触发更新 (
trigger(target, key)): 当设置属性值时,会调用trigger函数,通知所有依赖该属性的effect函数执行更新。
- 触发更新 (
-
deleteProperty(target, key): 拦截属性删除操作。- 触发更新 (
trigger(target, key)): 当删除属性时,会调用trigger函数,通知所有依赖该属性的effect函数执行更新。
- 触发更新 (
-
effect(fn): 接收一个函数作为参数,并立即执行该函数。- 将传入的函数包装成
effectFn,并将effectFn设置为当前激活的effect(activeEffect)。 - 执行
fn,触发依赖收集。 - 在
finally块中,将activeEffect设置为null,清除当前激活的effect。
- 将传入的函数包装成
-
track(target, key): 进行依赖收集。- 从
targetMap中获取目标对象的依赖映射表 (depsMap)。 - 如果
depsMap不存在,则创建一个新的Map对象,并将其添加到targetMap中。 - 从
depsMap中获取指定属性的依赖集合 (dep)。 - 如果
dep不存在,则创建一个新的Set对象,并将其添加到depsMap中。 - 将当前激活的
effect添加到dep中。
- 从
-
trigger(target, key): 触发更新。- 从
targetMap中获取目标对象的依赖映射表 (depsMap)。 - 如果
depsMap不存在,则直接返回。 - 从
depsMap中获取指定属性的依赖集合 (dep)。 - 如果
dep不存在,则直接返回。 - 遍历
dep中的所有effect函数,并执行它们。
- 从
依赖收集过程:
当执行 effect(() => { console.log(state.name); }) 时,会触发以下步骤:
effect函数执行,将activeEffect设置为当前effectFn。- 执行
console.log(state.name),触发state.name的getTrap。 getTrap 调用track(state, 'name')进行依赖收集。track函数将当前activeEffect(即effectFn)添加到state.name的依赖集合中。effect函数执行完毕,将activeEffect设置为null。
更新通知过程:
当执行 state.name = 'Jane' 时,会触发以下步骤:
- 触发
state.name的setTrap。 setTrap 调用trigger(state, 'name')触发更新。trigger函数获取state.name的依赖集合,并遍历其中的所有effect函数。- 执行依赖集合中的
effectFn,导致console.log(state.name)重新执行,输出新的值 ‘Jane’。
Proxy 实现深度依赖收集
Proxy 的一个重要优势是它可以轻松地实现深度依赖收集。在上面的示例中,get Trap 中递归调用了 reactive(res),这意味着如果属性值是对象,则会递归地将该对象转换为响应式对象。
当访问嵌套对象的属性时,也会触发依赖收集。例如,当执行 console.log(state.address.city) 时,会依次触发以下 Trap:
state.address的getTrap。address.city的getTrap。
这两个 Trap 都会进行依赖收集,确保当 state.address.city 的值发生变化时,依赖该属性的 effect 函数能够得到更新。
Proxy 与数组的响应式
Proxy 可以直接监听数组的变化,而不需要像 Vue 2 那样重写数组的方法。当使用数组的 push、pop、shift、unshift、splice 等方法修改数组时,会触发 set Trap,从而触发更新。
const data = {
items: ['a', 'b', 'c']
};
const state = reactive(data);
effect(() => {
console.log(`Items: ${state.items.join(', ')}`);
});
state.items.push('d'); // 输出: Items: a, b, c, d
state.items[0] = 'x'; // 输出: Items: x, b, c, d
一些值得注意的点
-
Reflect: 在get、set和deletePropertyTrap 中,我们使用了Reflect.get、Reflect.set和Reflect.deleteProperty。Reflect是 ES6 提供的一个内置对象,它提供了一组与对象操作相关的静态方法。使用Reflect的好处是可以保持默认的行为,并且可以正确处理this的指向问题。 -
WeakMap:reactiveMap和targetMap都使用了WeakMap。WeakMap是一种弱引用 Map,它的键是对象,值可以是任意类型。当键对象被垃圾回收时,WeakMap中对应的键值对也会被自动删除。使用WeakMap可以避免内存泄漏。 -
Set: 在track和trigger函数中,我们使用了Set来存储依赖集合。Set是一种不允许包含重复值的集合。使用Set可以避免重复添加依赖,提高性能。 -
防止无限循环: 在
triggerEffects中使用了new Set(dep),这是为了防止在effectFn执行过程中,又触发了新的依赖收集,导致无限循环。
总结:Proxy 带来了更强大的响应式能力
总而言之,Vue 3 使用 Proxy 替代了 Object.defineProperty,解决了 Object.defineProperty 的一些问题,带来了性能和功能上的提升。Proxy 可以监听整个对象,可以监听动态添加的属性,可以监听数组的变化,并且深度监听更加简洁。这些优势使得 Vue 3 的响应式系统更加强大和灵活。理解 Proxy 的 Trap 机制,对于我们深入理解 Vue 的响应式原理至关重要。掌握 Proxy 的使用,能帮助我们更好地理解和应用 Vue.js,甚至在其他场景下也能利用 Proxy 解决类似的问题。
更多IT精英技术系列讲座,到智猿学院