Vue3 的响应式系统源码级 Map/Set 的惰性代理与迭代器方法拦截原理
字数 1251 2025-11-26 20:27:50
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 实例本身:
const proxy = new Proxy(target, handlers); - 拦截
get方法:当访问map.get(key)时,才代理key对应的值。 - 内部维护一个
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 重写迭代器方法,返回一个 包装后的迭代器:
- 拦截原始迭代器方法:
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... } - 包装迭代器的
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 的写操作(如 set、add、delete)需要触发更新:
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. 总结:惰性代理与迭代器拦截的意义
- 性能优化:避免初始化时代理所有内部数据,按需转换减少开销。
- 依赖精确性:通过区分操作类型(如
addvsset),减少不必要的更新触发。 - 迭代器响应式:确保遍历操作能正确收集依赖,同时代理遍历到的值。
源码关键位置:
packages/reactivity/src/collectionHandlers.ts(mutableCollectionHandlers)packages/reactivity/src/reactive.ts(createReactiveObject)