Vue3 的响应式系统源码级 Set/Map 的响应式处理与 size 属性追踪原理
描述
Vue3 的响应式系统基于 Proxy 实现,对普通对象和数组的响应式处理已广为熟知。然而,JavaScript 内置的 Set 和 Map 集合类型具有特殊的 API(如 add、delete、forEach 等)和内部逻辑(如 size 访问器属性),其响应式转换需额外处理。面试官常考察:Vue3 如何让 Set/Map 变成响应式?size 属性如何被追踪?为何直接修改 size 无效?这涉及对 Proxy 拦截策略和依赖收集机制的深度理解。
解题过程
1. 核心挑战
Set/Map 与普通对象不同:
- 它们拥有专属方法(如
add、delete、clear、set、get等),这些方法会修改内部数据,但不会触发普通属性设置的 Proxy set 陷阱。 size是一个访问器属性(getter),每次访问时动态计算,需建立依赖追踪。- 迭代方法(如
forEach、keys、values)需处理内部数据变化与响应式更新的同步。
2. 响应式创建的入口
Vue3 的 reactive() 函数在处理目标对象时,会通过 createReactiveObject 创建 Proxy。对于 Set/Map 类型,会使用专门的 collectionHandlers(位于 packages/reactivity/src/collectionHandlers.ts),而非普通对象的 baseHandlers。
// 简化逻辑
const proxy = new Proxy(
target,
target instanceof Set || target instanceof Map
? collectionHandlers
: baseHandlers
)
3. collectionHandlers 的结构
collectionHandlers 包含 get、set(Map 的 set 方法)、has、add、delete、clear 等陷阱的拦截器。关键点:
get陷阱:拦截所有方法访问(如size、add、forEach)。- 对
size:通过track函数收集依赖(ITERATE_KEY作为追踪标识),因为size变化代表集合内容变化。 - 对变异方法(如
add、delete、set):执行原始方法后,手动调用trigger触发更新。
4. size 属性的依赖追踪
访问 size 时,get 陷阱被触发:
// 简化代码
get(target, key, receiver) {
if (key === 'size') {
track(target, ITERATE_KEY) // 依赖收集到 ITERATE_KEY
return Reflect.get(target, key, target) // 返回原始 size
}
// ... 处理其他方法
}
ITERATE_KEY 是一个特殊的 Symbol,代表“集合内容迭代变化”。任何改变集合内容的方法(如 add、delete)都会触发 ITERATE_KEY 的依赖,从而更新依赖 size 的副作用。
5. 变异方法的拦截与更新触发
以 Set 的 add 为例:
// 简化版 add 方法拦截
function add(this, value) {
const target = toRaw(this) // 获取原始对象
const hadKey = target.has(value)
const result = target.add(value) // 调用原始方法
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, value) // 触发更新
}
return result
}
trigger 会触发两类依赖:
- 与具体值相关的依赖(如通过
has(value)追踪的)。 ITERATE_KEY依赖(对应size和迭代方法)。
6. 避免重复触发与原始值获取
toRaw函数确保从响应式代理获取原始 Set/Map,防止在代理上重复操作。- 在
add前检查hadKey,避免重复添加时不必要的更新。
7. 迭代方法的处理
forEach、keys、values 等方法被调用时,会通过 get 陷阱返回一个包装函数。该包装函数在迭代时会追踪每个访问的条目,同时也会追踪 ITERATE_KEY(或 MAP_KEY_ITERATE_KEY),确保集合结构变化时能触发更新。
8. 深层响应式转换
若 Set/Map 存储对象,Vue3 会在访问时自动进行深层转换(通过 toReactive 函数),确保嵌套对象也是响应式的。
总结
Vue3 对 Set/Map 的响应式处理通过专门的 collectionHandlers 实现:
size依赖通过ITERATE_KEY收集,在内容变化时触发。- 变异方法被拦截,在执行原始操作后手动触发更新。
- 迭代方法被包装,确保依赖正确收集。
- 这保证了 Set/Map 能无缝融入 Vue3 响应式系统,同时保持原生 API 的行为一致性。