Vue3 的响应式系统源码级数组的 7 个变异方法的重写与依赖触发优化原理
题目描述
在 Vue3 的响应式系统中,数组是一种特殊的对象。为了实现对数组的响应式监听,Vue3 重写了 JavaScript 中 7 个会改变数组自身的方法,并确保通过这些方法修改数组时能正确触发依赖更新。我们将深入源码,探讨这 7 个变异方法是如何被重写的,以及 Vue3 在依赖触发方面做了哪些优化。
解题过程
1. 问题背景
JavaScript 数组的原生方法中,有 7 个会改变数组自身(变异方法):
push, pop, shift, unshift, splice, sort, reverse。
当我们用 reactive() 包装一个数组时,如果直接调用这些原生方法修改数组,Vue3 需要确保:
- 能正确追踪到数组的变化;
- 能触发相关的副作用(effect)重新执行;
- 避免因数组长度变化或索引变化导致的依赖遗漏。
由于 Vue3 的响应式系统基于 Proxy 实现,Proxy 能拦截对数组的读写操作,但无法直接拦截原生方法的调用。因此,Vue3 选择“重写”这 7 个方法,在方法调用时手动触发依赖更新。
2. 重写方法的源码位置
在 Vue3 源码的 packages/reactivity/src/arrayInstrumentations.ts 文件中,我们可以找到对这 7 个方法的重写实现。这个文件导出了一个对象 arrayInstrumentations,它包含了重写后的方法。
3. 重写方法的核心逻辑
我们以 push 方法为例,讲解重写过程:
// 伪代码,简化版本
const arrayInstrumentations: Record<string, Function> = {};
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
const original = Array.prototype[method]; // 保存原生方法
arrayInstrumentations[method] = function(this: unknown[], ...args: unknown[]) {
// 1. 暂停依赖收集(避免重复收集)
pauseTracking();
// 2. 执行原生方法
const res = original.apply(this, args);
// 3. 恢复依赖收集
resetTracking();
// 4. 手动触发依赖更新
trigger(this, TriggerOpTypes.ARRAY_MUTATION, { method, args, result: res });
return res;
};
});
关键点解析:
- 暂停依赖收集:调用原生方法时,可能会触发数组的
getter(例如访问length属性),如果不暂停收集,可能会收集到不必要的依赖。 - 执行原生方法:用
original.apply(this, args)确保数组被正确修改。 - 手动触发更新:通过
trigger函数通知所有依赖此数组的副作用重新执行。
4. 对 length 属性的特殊处理
数组的 length 属性是响应式的,但直接设置 arr.length = 0 也会触发更新。Vue3 在 Proxy 的 set 拦截器中已经能捕获对 length 的修改,无需在重写方法中额外处理。但重写的方法需要确保通过 splice 等方法隐式修改 length 时也能正确触发更新。
5. 依赖触发的优化策略
在 trigger 函数中,Vue3 对数组的变异方法调用做了特殊优化:
// 伪代码,简化版 trigger 逻辑
function trigger(target, type, key) {
if (type === TriggerOpTypes.ARRAY_MUTATION) {
// 对数组变异方法,除了触发 length 的依赖,还需要触发索引相关的依赖
const depsMap = targetMap.get(target);
if (depsMap) {
// 触发 length 的依赖
triggerEffects(depsMap.get('length'));
// 如果方法是 push/unshift/splice(新增元素),触发新增索引的依赖
if (method === 'push' || method === 'unshift') {
for (let i = args.length - 1; i >= 0; i--) {
triggerEffects(depsMap.get(i));
}
}
}
}
}
优化点:
- 只触发真正变化的部分(如新增的索引),避免不必要的更新。
- 对
push和unshift,需要触发新增索引的依赖,确保新增的元素也是响应式的。
6. 避免无限递归的防护
在重写的方法中,如果我们在 push 时又访问了数组的响应式属性,可能会造成递归触发。Vue3 通过 pauseTracking() 和 resetTracking() 来暂停和恢复依赖收集,避免在方法执行过程中收集到新的依赖,从而防止无限递归。
7. 重写方法的挂载时机
在创建数组的 Proxy 时,Vue3 会在 get 拦截器中判断访问的属性是否是这 7 个方法之一。如果是,则返回重写后的方法:
// 伪代码,简化版 get 拦截器
function createGetter() {
return function get(target, key, receiver) {
if (key === 'push' || key === 'pop' /* ...其他6个方法 */) {
// 返回重写后的方法
return arrayInstrumentations[key];
}
// 正常处理其他属性...
};
}
这样,当我们调用 arr.push() 时,实际上调用的是重写后的方法。
总结
Vue3 通过重写数组的 7 个变异方法,在方法调用时手动触发依赖更新,并结合 pauseTracking/resetTracking 防止过度收集依赖,实现了对数组变化的精准响应。这种设计既兼容了 JavaScript 数组的原生行为,又确保了响应式系统的正确性和性能。