Vue3 的响应式系统源码级数组的 7 个变异方法的重写与依赖触发优化原理
字数 1609 2025-12-12 17:20:04

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));
        }
      }
    }
  }
}

优化点

  • 只触发真正变化的部分(如新增的索引),避免不必要的更新。
  • pushunshift,需要触发新增索引的依赖,确保新增的元素也是响应式的。

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 数组的原生行为,又确保了响应式系统的正确性和性能。

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 方法为例,讲解重写过程: 关键点解析 : 暂停依赖收集 :调用原生方法时,可能会触发数组的 getter (例如访问 length 属性),如果不暂停收集,可能会收集到不必要的依赖。 执行原生方法 :用 original.apply(this, args) 确保数组被正确修改。 手动触发更新 :通过 trigger 函数通知所有依赖此数组的副作用重新执行。 4. 对 length 属性的特殊处理 数组的 length 属性是响应式的,但直接设置 arr.length = 0 也会触发更新。Vue3 在 Proxy 的 set 拦截器中已经能捕获对 length 的修改,无需在重写方法中额外处理。但重写的方法需要确保通过 splice 等方法隐式修改 length 时也能正确触发更新。 5. 依赖触发的优化策略 在 trigger 函数中,Vue3 对数组的变异方法调用做了特殊优化: 优化点 : 只触发真正变化的部分(如新增的索引),避免不必要的更新。 对 push 和 unshift ,需要触发新增索引的依赖,确保新增的元素也是响应式的。 6. 避免无限递归的防护 在重写的方法中,如果我们在 push 时又访问了数组的响应式属性,可能会造成递归触发。Vue3 通过 pauseTracking() 和 resetTracking() 来暂停和恢复依赖收集,避免在方法执行过程中收集到新的依赖,从而防止无限递归。 7. 重写方法的挂载时机 在创建数组的 Proxy 时,Vue3 会在 get 拦截器中判断访问的属性是否是这 7 个方法之一。如果是,则返回重写后的方法: 这样,当我们调用 arr.push() 时,实际上调用的是重写后的方法。 总结 Vue3 通过重写数组的 7 个变异方法,在方法调用时手动触发依赖更新,并结合 pauseTracking / resetTracking 防止过度收集依赖,实现了对数组变化的精准响应。这种设计既兼容了 JavaScript 数组的原生行为,又确保了响应式系统的正确性和性能。