Vue3 的响应式系统源码级 Map/Set 的惰性代理与迭代器方法拦截原理
字数 1251 2025-11-26 20:27:50

Vue3 的响应式系统源码级 Map/Set 的惰性代理与迭代器方法拦截原理

1. 背景与问题描述

Vue3 的响应式系统基于 Proxy 实现,但 MapSet 这类集合类型存在特殊行为:

  • 它们的方法(如 map.set()set.add())可能触发多次数据访问(例如 gethas)。
  • 迭代器方法(如 keys()values()entries())需要拦截以建立依赖关系。
  • 直接代理所有内部方法会导致不必要的性能开销(如惰性访问的键未被使用时也被代理)。

核心问题:如何高效代理 Map/Set 的方法和迭代器,同时避免过度响应式化?


2. 惰性代理(Lazy Proxy)机制

2.1 为什么需要惰性代理?

Map/Set 的键可能是对象,但并非所有键都会被访问。若初始化时直接代理所有键,会导致:

  • 内存浪费:未被访问的键也被转换为响应式对象。
  • 性能损耗:递归代理复杂对象的开销较大。

2.2 实现方式

Vue3 通过 按需代理 解决:

  1. 代理 Map/Set 实例本身
    const proxy = new Proxy(target, handlers);  
    
  2. 拦截 get 方法:当访问 map.get(key) 时,才代理 key 对应的值。
  3. 内部维护一个 rawToReactive 弱引用表:缓存原始对象到响应式对象的映射,避免重复代理。

示例代码逻辑

const reactiveMap = new WeakMap(); // 缓存表  

function reactive(target) {  
  if (target instanceof Map || target instanceof Set) {  
    return createCollectionProxy(target, reactiveMap);  
  }  
  // 处理普通对象...  
}  

function createCollectionProxy(target, proxyMap) {  
  // 若已代理过,直接返回缓存结果  
  if (proxyMap.has(target)) return proxyMap.get(target);  

  const proxy = new Proxy(target, {  
    get(target, key, receiver) {  
      // 拦截方法调用(如 get、add、delete)  
      if (key === 'get') {  
        return function (key) {  
          // 惰性代理:仅当访问具体键值时才对值进行响应式转换  
          const rawValue = target.get(key);  
          return isObject(rawValue) ? reactive(rawValue) : rawValue;  
        };  
      }  
      // 处理其他方法...  
    }  
  });  

  proxyMap.set(target, proxy);  
  return proxy;  
}  

3. 迭代器方法拦截

3.1 迭代器的依赖收集挑战

Map/Set 的 keys()values()entries() 方法返回迭代器,直接遍历可能涉及多个键值对。需确保:

  • 遍历过程中访问的每个键值都能被依赖追踪。
  • 迭代器本身被访问时,建立与整个集合的依赖关系。

3.2 拦截方案

Vue3 重写迭代器方法,返回一个 包装后的迭代器

  1. 拦截原始迭代器方法
    function get(target, key, receiver) {  
      if (key === 'keys') {  
        return function () {  
          // 建立整个集合的依赖关系(用于监听 size 变化)  
          track(target, 'iterate');  
          return target.keys();  
        };  
      }  
      if (key === 'values') {  
        return function () {  
          track(target, 'iterate');  
          const rawIterator = target.values();  
          // 返回包装迭代器,在遍历时代理每个值  
          return createReactiveIterator(rawIterator, target);  
        };  
      }  
      // 类似处理 entries...  
    }  
    
  2. 包装迭代器的 next() 方法
    function createReactiveIterator(iterator, source) {  
      return {  
        next() {  
          const { value, done } = iterator.next();  
          if (!done) {  
            // 对遍历到的值进行响应式转换  
            return { value: reactive(value), done };  
          }  
          return { value, done };  
        }  
      };  
    }  
    

4. 方法调用的副作用追踪

4.1 关键方法拦截

Map/Set 的写操作(如 setadddelete)需要触发更新:

function get(target, key, receiver) {  
  if (key === 'set') {  
    return function (key, value) {  
      const hadKey = target.has(key);  
      const oldValue = target.get(key);  
      target.set(key, value);  
      // 触发更新:区分新增和修改  
      if (!hadKey) {  
        trigger(target, 'add', key);  
      } else if (oldValue !== value) {  
        trigger(target, 'set', key);  
      }  
    };  
  }  
  // 类似处理 delete、clear...  
}  

4.2 依赖类型区分

  • 'add'/'delete':影响 size,需触发 'iterate' 相关的副作用(如遍历操作)。
  • 'set':仅修改值,不影响键集合,不触发 'iterate' 依赖。

5. 总结:惰性代理与迭代器拦截的意义

  1. 性能优化:避免初始化时代理所有内部数据,按需转换减少开销。
  2. 依赖精确性:通过区分操作类型(如 add vs set),减少不必要的更新触发。
  3. 迭代器响应式:确保遍历操作能正确收集依赖,同时代理遍历到的值。

源码关键位置

  • packages/reactivity/src/collectionHandlers.tsmutableCollectionHandlers
  • packages/reactivity/src/reactive.tscreateReactiveObject
Vue3 的响应式系统源码级 Map/Set 的惰性代理与迭代器方法拦截原理 1. 背景与问题描述 Vue3 的响应式系统基于 Proxy 实现,但 Map 和 Set 这类集合类型存在特殊行为: 它们的方法(如 map.set() 、 set.add() )可能触发多次数据访问(例如 get 和 has )。 迭代器方法(如 keys() 、 values() 、 entries() )需要拦截以建立依赖关系。 直接代理所有内部方法会导致不必要的性能开销(如惰性访问的键未被使用时也被代理)。 核心问题 :如何高效代理 Map/Set 的方法和迭代器,同时避免过度响应式化? 2. 惰性代理(Lazy Proxy)机制 2.1 为什么需要惰性代理? Map/Set 的键可能是对象,但并非所有键都会被访问。若初始化时直接代理所有键,会导致: 内存浪费 :未被访问的键也被转换为响应式对象。 性能损耗 :递归代理复杂对象的开销较大。 2.2 实现方式 Vue3 通过 按需代理 解决: 代理 Map/Set 实例本身 : 拦截 get 方法 :当访问 map.get(key) 时,才代理 key 对应的值。 内部维护一个 rawToReactive 弱引用表 :缓存原始对象到响应式对象的映射,避免重复代理。 示例代码逻辑 : 3. 迭代器方法拦截 3.1 迭代器的依赖收集挑战 Map/Set 的 keys() 、 values() 、 entries() 方法返回迭代器,直接遍历可能涉及多个键值对。需确保: 遍历过程中访问的每个键值都能被依赖追踪。 迭代器本身被访问时,建立与整个集合的依赖关系。 3.2 拦截方案 Vue3 重写迭代器方法,返回一个 包装后的迭代器 : 拦截原始迭代器方法 : 包装迭代器的 next() 方法 : 4. 方法调用的副作用追踪 4.1 关键方法拦截 Map/Set 的写操作(如 set 、 add 、 delete )需要触发更新: 4.2 依赖类型区分 'add' / 'delete' :影响 size ,需触发 'iterate' 相关的副作用(如遍历操作)。 'set' :仅修改值,不影响键集合,不触发 'iterate' 依赖。 5. 总结:惰性代理与迭代器拦截的意义 性能优化 :避免初始化时代理所有内部数据,按需转换减少开销。 依赖精确性 :通过区分操作类型(如 add vs set ),减少不必要的更新触发。 迭代器响应式 :确保遍历操作能正确收集依赖,同时代理遍历到的值。 源码关键位置 : packages/reactivity/src/collectionHandlers.ts ( mutableCollectionHandlers ) packages/reactivity/src/reactive.ts ( createReactiveObject )