在上一篇文章中,我们深入探讨了
track函数如何收集依赖。但收集只是开始,当数据变化时,如何精准地找到所有依赖并触发更新,才是响应式系统的"临门一脚"。本文将剖析trigger函数的完整实现,揭示 Vue3 如何优雅地处理各种更新场景。
前言:从修改数据到视图更新
当我们执行这样一行代码:
state.count++;
Vue3 内部会经历一个完整的链路:
- 触发
set拦截器:state.count的赋值操作被Proxy捕获 - 调用
trigger函数:根据修改的target和key查找依赖 - 执行副作用函数:找到所有依赖并重新执行
- 视图更新:如果是组件的渲染函数,就会触发 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 会陷入无限循环:
- 读取 state.count(收集依赖)
- 执行 state.count++(触发更新)
- 更新导致 effect 重新执行
- 回到步骤 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 的实现原理,不仅能帮助我们在遇到响应式问题时快速定位,还能在性能优化时做出更明智的选择。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!