trigger函数:更新触发的完整实现

6 阅读8分钟

在上一篇文章中,我们深入探讨了 track 函数如何收集依赖。但收集只是开始,当数据变化时,如何精准地找到所有依赖并触发更新,才是响应式系统的"临门一脚"。本文将剖析 trigger 函数的完整实现,揭示 Vue3 如何优雅地处理各种更新场景。

前言:从修改数据到视图更新

当我们执行这样一行代码:

state.count++;

Vue3 内部会经历一个完整的链路:

  1. 触发 set 拦截器:state.count 的赋值操作被 Proxy 捕获
  2. 调用 trigger 函数:根据修改的 targetkey 查找依赖
  3. 执行副作用函数:找到所有依赖并重新执行
  4. 视图更新:如果是组件的渲染函数,就会触发 DOM 更新

这个链路中最关键的一环就是 trigger 函数。它需要处理各种复杂的边界情况:

  • 普通属性的修改
  • 数组索引和 length 的变化
  • Map/Set 等集合类型的特殊操作
  • 避免无限循环的守卫机制

trigger 函数的基本架构

基础版本

让我们从一个最简版本的 trigger 开始:

// 全局的依赖存储
const targetMap = new WeakMap();

/**
 * 触发更新
 * @param {Object} target 原始对象
 * @param {string|symbol} key 修改的属性名
 */
function trigger(target, key) {
  // 1. 获取 target 对应的依赖 Map
  const depsMap = targetMap.get(target);
  if (!depsMap) return; // 没有依赖,直接返回
  
  // 2. 获取 key 对应的依赖 Set
  const dep = depsMap.get(key);
  if (!dep) return; // 没有依赖,直接返回
  
  // 3. 创建新 Set 执行,避免无限循环
  const effectsToRun = new Set(dep);
  
  // 4. 执行所有副作用
  effectsToRun.forEach(effect => {
    effect.run();
  });
}

这个基础版本看似简单,但它已经能处理最基本的更新场景。然而,真实世界的复杂性远超这个简单实现。

根据 key 查找依赖

不同类型的 key

在实际应用中,我们需要处理多种类型的 key:

1. 处理普通 key

const dep = depsMap.get(key);
if (dep) {
  dep.forEach(effect => effectsToRun.add(effect));
}

2. 处理数组的特殊情况

if (Array.isArray(target) && key === 'length') {
  // 数组 length 变化需要特殊处理
  handleArrayLengthChange(depsMap, newValue, effectsToRun);
}

3. 处理迭代相关的 key (for...in)

if (type === 'add' || type === 'delete') {
  const iterateEffects = depsMap.get(ITERATE_KEY);
  if (iterateEffects) {
    iterateEffects.forEach(effect => effectsToRun.add(effect));
  }
}

4. 处理 Map/Set 的特殊 key

if (isCollection(target)) {
  handleCollectionChange(target, type, key, depsMap, effectsToRun);
}

ITERATE_KEY:追踪对象的迭代

当使用 for...in 遍历对象时,我们需要追踪对象所有属性的变化:

const state = reactive({ a: 1, b: 2 });

effect(() => {
  for (const key in state) {
    console.log(key); // 应该在任何属性变化时重新执行
  }
});

state.c = 3; // 添加属性,应该触发上面的 effect

为了实现这个,Vue3 使用了一个特殊的 Symbol 作为 key:

// 特殊标识符,用于追踪对象的迭代操作
const ITERATE_KEY = Symbol('iterate');

// 在 track 中收集迭代依赖
function track(target, type, key) {
  if (type === TrackOpTypes.ITERATE) {
    // 迭代操作使用 ITERATE_KEY 作为 key
    key = ITERATE_KEY;
  }
  // ... 收集逻辑
}

// 在 trigger 中触发迭代依赖
function trigger(target, type, key, newValue) {
  // 新增或删除属性时,需要触发迭代依赖
  if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
    const iterateEffects = depsMap.get(ITERATE_KEY);
    if (iterateEffects) {
      iterateEffects.forEach(effect => effectsToRun.add(effect));
    }
  }
}

处理 length 变化的完整逻辑

function handleArrayLengthChange(depsMap, newLength, effectsToRun) {
  // 1. 先添加 length 自身的依赖
  const lengthDep = depsMap.get('length');
  if (lengthDep) {
    lengthDep.forEach(effect => effectsToRun.add(effect));
  }
  
  // 2. 找到所有索引 >= 新长度的依赖
  // 因为修改 length 会删除这些索引的元素
  depsMap.forEach((dep, key) => {
    // 检查 key 是否为数字索引
    if (isArrayIndex(key)) {
      const index = Number(key);
      if (index >= newLength) {
        // 这个索引会被删除,需要触发它的依赖
        dep.forEach(effect => effectsToRun.add(effect));
      }
    }
  });
}

/**
 * 判断是否为有效的数组索引
 */
function isArrayIndex(key) {
  // 检查 key 是否为正整数且小于安全整数
  if (typeof key !== 'string') return false;
  
  const keyAsNumber = Number(key);
  return Number.isInteger(keyAsNumber) &&
         keyAsNumber >= 0 &&
         keyAsNumber < Number.MAX_SAFE_INTEGER;
}

数组索引变化的处理

function trigger(target, key, type, newValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  const effectsToRun = new Set();
  
  // 处理直接依赖
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effectsToRun.add(effect));
  }
  
  // 如果是数组且 key 是数字索引
  if (Array.isArray(target) && isArrayIndex(key)) {
    const index = Number(key);
    
    // 如果添加了新元素(索引 >= 原 length)
    if (type === TriggerOpTypes.ADD && index >= target.length - 1) {
      // 触发 length 的依赖
      const lengthDep = depsMap.get('length');
      if (lengthDep) {
        lengthDep.forEach(effect => effectsToRun.add(effect));
      }
    }
  }
  
  runEffects(effectsToRun);
}

避免循环触发

问题的产生

考虑这个例子:

const state = reactive({ count: 0 });

effect(() => {
  state.count++; // 在 effect 中修改依赖的属性
});

这个 effect 会陷入无限循环:

  1. 读取 state.count(收集依赖)
  2. 执行 state.count++(触发更新)
  3. 更新导致 effect 重新执行
  4. 回到步骤 1

解决方案:activeEffect 守卫

// 在 trigger 中,避免重复执行当前正在执行的 effect
function runEffects(effectsToRun) {
  effectsToRun.forEach(effect => {
    // 关键检查:如果当前执行的 effect 就是正在触发的 effect,跳过
    if (effect !== activeEffect) {
      effect.run();
    }
  });
}

// 更完整的版本
function triggerEffects(dep) {
  const effects = new Set(dep);
  
  effects.forEach(effect => {
    // 如果 effect 正在执行中,跳过本次触发
    if (effect === activeEffect) {
      console.log('跳过当前执行的 effect,避免循环');
      return;
    }
    
    // 如果 effect 已经被调度,使用调度器
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      effect.run();
    }
  });
}

更深层的循环:相互依赖

还有更复杂的场景:

const state = reactive({ a: 1, b: 1 });

effect(() => {
  state.b = state.a + 1; // effect1
});

effect(() => {
  state.a = state.b + 1; // effect2
});

这种情况下,两个 effect 相互触发,仍然可能导致无限循环。Vue3 通过递归深度限制标记机制来防范:

class ReactiveEffect {
  // 记录递归深度
  runDepth = 0;
  
  run() {
    try {
      this.runDepth++;
      if (this.runDepth > MAX_RECURSION_DEPTH) {
        console.warn('检测到可能的无限循环');
        return;
      }
      
      activeEffect = this;
      return this.fn();
    } finally {
      this.runDepth--;
      activeEffect = null;
    }
  }
}

集合类型的特殊处理

Map/Set 的操作类型

集合类型的操作更加丰富,需要更多类型的触发:

// 操作类型枚举
const TriggerOpTypes = {
  SET: 'set',       // 修改现有值
  ADD: 'add',       // 添加新值
  DELETE: 'delete', // 删除值
  CLEAR: 'clear'    // 清空集合
};

Map 的 set 操作

function handleMapSet(target, key, depsMap, effectsToRun) {
  // 获取 key 的依赖
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effectsToRun.add(effect));
  }
  
  // Map 的迭代依赖(size 属性)
  const iterateDep = depsMap.get(ITERATE_KEY);
  if (iterateDep) {
    iterateDep.forEach(effect => effectsToRun.add(effect));
  }
  
  // size 属性的依赖
  const sizeDep = depsMap.get('size');
  if (sizeDep) {
    sizeDep.forEach(effect => effectsToRun.add(effect));
  }
}

Set 的 add 操作

function handleSetAdd(target, value, depsMap, effectsToRun) {
  // Set 没有 key 的概念,但需要触发迭代依赖
  const iterateDep = depsMap.get(ITERATE_KEY);
  if (iterateDep) {
    iterateDep.forEach(effect => effectsToRun.add(effect));
  }
  
  // size 依赖
  const sizeDep = depsMap.get('size');
  if (sizeDep) {
    sizeDep.forEach(effect => effectsToRun.add(effect));
  }
  
  // 如果值已存在,还需要触发值本身的依赖
  const valueDep = depsMap.get(value);
  if (valueDep) {
    valueDep.forEach(effect => effectsToRun.add(effect));
  }
}

手写实现:完整的 trigger 函数

现在让我们将所有部分组合起来,实现一个完整的 trigger 函数:

// 触发操作类型枚举
const TriggerOpTypes = {
  SET: 'set',
  ADD: 'add',
  DELETE: 'delete',
  CLEAR: 'clear'
};

// 全局变量
const targetMap = new WeakMap();
let activeEffect = null;

/**
 * 完整的 trigger 实现
 */
function trigger(target, type, key, newValue, oldValue) {
  // 1. 获取依赖 Map
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    // 没有依赖,直接返回
    return;
  }
  
  // 2. 创建集合存储需要执行的 effects
  const effectsToRun = new Set();
  
  // 3. 定义添加 effect 的辅助函数
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        // 避免当前正在执行的 effect 被重复执行
        if (effect !== activeEffect) {
          effectsToRun.add(effect);
        }
      });
    }
  };
  
  // 4. 处理普通 key 的依赖
  if (key !== undefined) {
    add(depsMap.get(key));
  }
  
  // 5. 处理数组的特殊情况
  if (Array.isArray(target)) {
    handleArrayTrigger(target, type, key, newValue, depsMap, add);
  } else {
    // 6. 处理集合类型的特殊情况
    handleCollectionTrigger(target, type, key, depsMap, add);
  }
  
  // 7. 执行所有 effects
  runEffects(effectsToRun);
}

/**
 * 处理数组的 trigger
 */
function handleArrayTrigger(target, type, key, newValue, depsMap, add) {
  // 情况 1:修改 length
  if (key === 'length') {
    // 添加 length 本身的依赖
    add(depsMap.get('length'));
    
    // 找到所有索引 >= 新 length 的依赖
    const newLength = Number(newValue);
    depsMap.forEach((dep, key) => {
      if (isArrayIndex(key)) {
        const index = Number(key);
        if (index >= newLength) {
          add(dep);
        }
      }
    });
  } 
  // 情况 2:添加数组元素(索引 >= 原 length)
  else if (type === TriggerOpTypes.ADD && isArrayIndex(key)) {
    add(depsMap.get('length'));
  }
}

/**
 * 处理集合类型的 trigger
 */
function handleCollectionTrigger(target, type, key, depsMap, add) {
  // 新增或删除操作需要触发迭代依赖
  if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
    add(depsMap.get(ITERATE_KEY));
    
    // Map 的 size 属性也需要触发
    if (type === TriggerOpTypes.ADD && isMap(target)) {
      add(depsMap.get('size'));
    }
  }
  
  // 清空操作需要触发所有迭代依赖
  if (type === TriggerOpTypes.CLEAR) {
    depsMap.forEach((dep, mapKey) => {
      if (mapKey === ITERATE_KEY || mapKey === 'size') {
        add(dep);
      }
    });
  }
}

/**
 * 执行所有 effects
 */
function runEffects(effectsToRun) {
  // 转换为数组并排序(保证执行顺序的一致性)
  const effects = Array.from(effectsToRun);
  
  effects.forEach(effect => {
    if (effect.scheduler) {
      // 如果有调度器,使用调度器执行
      effect.scheduler();
    } else {
      // 直接执行
      effect.run();
    }
  });
}

/**
 * 判断是否为有效的数组索引
 */
function isArrayIndex(key) {
  if (typeof key !== 'string') return false;
  
  const keyAsNumber = Number(key);
  return Number.isInteger(keyAsNumber) &&
         keyAsNumber >= 0 &&
         keyAsNumber < Number.MAX_SAFE_INTEGER;
}

/**
 * 判断是否为 Map
 */
function isMap(target) {
  return target && target instanceof Map;
}

源码对标:Vue3 的 trigger 逻辑

Vue3 源码中的 trigger 实现更加完善,我们来看几个关键点的对比:

1. 更精细的依赖分类

// Vue3 源码中的依赖分类(简化版)
function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  // 分类存储不同的依赖
  const effectsToRun = new Set();
  let deps = [];
  
  // 根据操作类型收集不同的依赖
  if (type === TriggerOpTypes.CLEAR) {
    // 清空操作需要所有依赖
    deps = [...depsMap.values()];
  } else if (key === 'length' && Array.isArray(target)) {
    // 数组 length 的特殊处理
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= newValue) {
        deps.push(dep);
      }
    });
  } else {
    // 普通操作
    if (key !== undefined) {
      deps.push(depsMap.get(key));
    }
    
    // 迭代依赖
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!Array.isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY));
        } else if (Array.isArray(target) && isArrayIndex(key)) {
          // 数组添加元素触发 length
          deps.push(depsMap.get('length'));
        }
        break;
      case TriggerOpTypes.DELETE:
        if (!Array.isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY));
        }
        break;
      case TriggerOpTypes.SET:
        if (target instanceof Map) {
          deps.push(depsMap.get(ITERATE_KEY));
        }
        break;
    }
  }
  
  // 合并并执行
  for (const dep of deps) {
    if (dep) {
      dep.forEach(effect => {
        if (effect !== activeEffect) {
          effectsToRun.add(effect);
        }
      });
    }
  }
  
  // 执行 effects
  runEffects(effectsToRun);
}

2. 调度器与队列

Vue3 通过调度器实现异步更新和批量处理:

// Vue3 的调度器示例
let queue = [];
let isFlushing = false;

function queueJob(job) {
  if (!queue.includes(job)) {
    queue.push(job);
    queueFlush();
  }
}

function queueFlush() {
  if (!isFlushing) {
    isFlushing = true;
    Promise.resolve().then(flushJobs);
  }
}

function flushJobs() {
  try {
    for (let i = 0; i < queue.length; i++) {
      queue[i]();
    }
  } finally {
    queue.length = 0;
    isFlushing = false;
  }
}

// 在 trigger 中使用
if (effect.scheduler) {
  effect.scheduler(); // 可能使用 queueJob
} else {
  effect.run();
}

常见问题排查

问题 1:修改数据但视图不更新

const arr = reactive([1, 2, 3]);

arr[5] = 6; // 错误:索引 5 超出当前 length
// 可能的问题:trigger 没有正确处理数组索引

arr.push(6); // 正确:使用数组方法
// 或者
arr.length = 6;
arr[5] = 6;

问题 2:不必要的频繁更新

const state = reactive({ count: 0 });
setInterval(() => {
  state.count++; // 每次修改都会触发更新,可能造成性能问题
}, 10);

let count = 0;
const state = reactive({ count: 0 });
setInterval(() => {
  count++;
  // 使用调度器节流,批量更新
  if (count % 100 === 0) {
    state.count = count;
  }
}, 10);

问题 3:无限循环的检测

// 添加循环检测
function runEffects(effectsToRun) {
  const runStack = new Set();
  
  effectsToRun.forEach(effect => {
    if (runStack.has(effect)) {
      console.warn('检测到循环依赖');
      return;
    }
    
    runStack.add(effect);
    try {
      effect.run();
    } finally {
      runStack.delete(effect);
    }
  });
}

结语

trigger 函数作为响应式系统的"执行者",承担着从数据变化到副作用执行的桥梁作用。理解 trigger 的实现原理,不仅能帮助我们在遇到响应式问题时快速定位,还能在性能优化时做出更明智的选择。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!