深入理解Proxy的Trap机制:Vue如何拦截`get`/`set`/`deleteProperty`实现深度依赖收集

深入理解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.getOwnPropertyNamesObject.getOwnPropertySymbols 获取对象的所有自身属性键
apply 调用函数
construct 使用 new 操作符调用构造函数

Vue.js 主要利用了 getsetdeleteProperty 这三个 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 来劫持对象的属性,从而实现响应式。它通过 gettersetter 函数来监听属性的读取和修改。

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 的 getsetdeleteProperty 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, 但删除操作已经生效

代码解释:

  1. reactive(target): 接收一个对象作为参数,返回该对象的响应式代理。

    • 如果传入的不是对象,则直接返回。
    • 使用 WeakMap (reactiveMap) 缓存已经创建的 Proxy,避免重复代理同一个对象。
    • 创建 Proxy 对象,并定义 getsetdeleteProperty Trap。
    • 将原始对象和代理对象存储在 reactiveMap 中。
  2. get(target, key, receiver): 拦截属性读取操作。

    • 依赖收集 (track(target, key)): 当读取属性时,会调用 track 函数,将当前激活的 effect 函数(如果有)添加到该属性的依赖集合中。
    • 递归代理 (reactive(res)): 如果读取的属性值是对象,则递归调用 reactive 函数,将其转换为响应式对象。
  3. set(target, key, value, receiver): 拦截属性设置操作。

    • 触发更新 (trigger(target, key)): 当设置属性值时,会调用 trigger 函数,通知所有依赖该属性的 effect 函数执行更新。
  4. deleteProperty(target, key): 拦截属性删除操作。

    • 触发更新 (trigger(target, key)): 当删除属性时,会调用 trigger 函数,通知所有依赖该属性的 effect 函数执行更新。
  5. effect(fn): 接收一个函数作为参数,并立即执行该函数。

    • 将传入的函数包装成 effectFn,并将 effectFn 设置为当前激活的 effect (activeEffect)。
    • 执行 fn,触发依赖收集。
    • finally 块中,将 activeEffect 设置为 null,清除当前激活的 effect
  6. track(target, key): 进行依赖收集。

    • targetMap 中获取目标对象的依赖映射表 (depsMap)。
    • 如果 depsMap 不存在,则创建一个新的 Map 对象,并将其添加到 targetMap 中。
    • depsMap 中获取指定属性的依赖集合 (dep)。
    • 如果 dep 不存在,则创建一个新的 Set 对象,并将其添加到 depsMap 中。
    • 将当前激活的 effect 添加到 dep 中。
  7. trigger(target, key): 触发更新。

    • targetMap 中获取目标对象的依赖映射表 (depsMap)。
    • 如果 depsMap 不存在,则直接返回。
    • depsMap 中获取指定属性的依赖集合 (dep)。
    • 如果 dep 不存在,则直接返回。
    • 遍历 dep 中的所有 effect 函数,并执行它们。

依赖收集过程:

当执行 effect(() => { console.log(state.name); }) 时,会触发以下步骤:

  1. effect 函数执行,将 activeEffect 设置为当前 effectFn
  2. 执行 console.log(state.name),触发 state.nameget Trap。
  3. get Trap 调用 track(state, 'name') 进行依赖收集。
  4. track 函数将当前 activeEffect(即 effectFn)添加到 state.name 的依赖集合中。
  5. effect 函数执行完毕,将 activeEffect 设置为 null

更新通知过程:

当执行 state.name = 'Jane' 时,会触发以下步骤:

  1. 触发 state.nameset Trap。
  2. set Trap 调用 trigger(state, 'name') 触发更新。
  3. trigger 函数获取 state.name 的依赖集合,并遍历其中的所有 effect 函数。
  4. 执行依赖集合中的 effectFn,导致 console.log(state.name) 重新执行,输出新的值 ‘Jane’。

Proxy 实现深度依赖收集

Proxy 的一个重要优势是它可以轻松地实现深度依赖收集。在上面的示例中,get Trap 中递归调用了 reactive(res),这意味着如果属性值是对象,则会递归地将该对象转换为响应式对象。

当访问嵌套对象的属性时,也会触发依赖收集。例如,当执行 console.log(state.address.city) 时,会依次触发以下 Trap:

  1. state.addressget Trap。
  2. address.cityget Trap。

这两个 Trap 都会进行依赖收集,确保当 state.address.city 的值发生变化时,依赖该属性的 effect 函数能够得到更新。

Proxy 与数组的响应式

Proxy 可以直接监听数组的变化,而不需要像 Vue 2 那样重写数组的方法。当使用数组的 pushpopshiftunshiftsplice 等方法修改数组时,会触发 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:getsetdeleteProperty Trap 中,我们使用了 Reflect.getReflect.setReflect.deletePropertyReflect 是 ES6 提供的一个内置对象,它提供了一组与对象操作相关的静态方法。使用 Reflect 的好处是可以保持默认的行为,并且可以正确处理 this 的指向问题。

  • WeakMap: reactiveMaptargetMap 都使用了 WeakMapWeakMap 是一种弱引用 Map,它的键是对象,值可以是任意类型。当键对象被垃圾回收时,WeakMap 中对应的键值对也会被自动删除。使用 WeakMap 可以避免内存泄漏。

  • Set:tracktrigger 函数中,我们使用了 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精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注