Vue.js设计与实现 第四章:响应系统的作用与实现
Vue.js 响应系统的核心概念:响应式数据与副作用函数
在 Vue.js 中,响应系统是实现数据动态更新的关键机制。它允许开发者通过简单的数据绑定实现视图的自动更新。为了更好地理解 Vue.js 的响应系统,我们需要从两个核心概念入手:响应式数据和副作用函数。
1. 副作用函数:理解其作用与影响
副作用函数是指那些在执行过程中会对外部环境产生影响的函数。例如,修改全局变量、操作 DOM 或发送网络请求等操作都属于副作用。以下是一个简单的副作用函数示例:
// 全局变量
let val = 1;
function effect() {
val = 2; // 修改全局变量,产生副作用
}
在这个例子中,effect
函数通过修改全局变量 val
的值,对程序的外部状态产生了影响。这种修改是不可预测的,因为它可能会被其他函数读取或依赖。因此,副作用函数的执行需要谨慎处理,以避免潜在的冲突。
另一个常见的副作用函数是操作 DOM。例如:
function effect() {
document.body.innerText = 'hello vue3'; // 修改 DOM 内容,产生副作用
}
当 effect
函数执行时,它会直接修改页面的文本内容。这种操作会影响页面的显示状态,从而对其他依赖 DOM 的代码产生潜在影响。
2. 响应式数据:实现数据驱动的自动更新
响应式数据是 Vue.js 的核心特性之一。它允许开发者将数据与视图绑定,当数据发生变化时,视图会自动更新。响应式数据的关键在于数据对象的“响应式化”,即当数据对象的属性值发生变化时,能够自动触发相关的副作用函数重新执行。
以下是一个简单的响应式数据示例:
const obj = { text: 'hello world' };
function effect() {
document.body.innerText = obj.text; // 读取 obj.text 并设置 DOM 内容
}
在这个例子中,effect
函数读取了 obj.text
的值,并将其设置为页面的文本内容。然而,目前的 obj
是一个普通对象,当我们修改 obj.text
的值时,effect
函数并不会自动重新执行。为了实现响应式数据,我们需要让 obj
成为一个响应式对象。
3. 从普通对象到响应式数据
目前,obj
是一个普通对象,无法实现响应式更新。为了将其转变为响应式数据,我们需要借助 Proxy
或其他机制来拦截属性的读取和修改操作。例如:
const obj = { text: 'hello world' };
function effect() {
document.body.innerText = obj.text; // 读取 obj.text 并设置 DOM 内容
}
obj.text = 'hello vue3'; // 修改 obj.text 的值,希望副作用函数重新执行
在上述代码中,我们希望当 obj.text
的值发生变化时,effect
函数能够自动重新执行。然而,目前的代码无法实现这一目标,因为 obj
是一个普通对象。在接下来的章节中,我们将深入探讨如何通过 Proxy
实现响应式数据,并解决上述提到的挑战。
响应式数据的基本实现:从原理到代码
在上一节中,我们介绍了响应式数据和副作用函数的概念,并探讨了它们在 Vue.js 中的重要性。接下来,我们将深入探讨如何通过代码实现一个简单的响应式数据系统。这一节将通过具体的代码示例,展示如何利用 JavaScript 的 Proxy
实现响应式数据的基本功能。
1. 响应式数据的核心原理
要实现响应式数据,我们需要解决两个关键问题:
如何在读取数据时记录副作用函数?
当副作用函数读取某个数据时,我们需要记录这个副作用函数,以便在数据更新时能够重新执行它。如何在更新数据时触发副作用函数的执行重新?
当数据发生变化时,我们需要找到所有依赖该数据的副作用函数,并触发它们重新执行。
2. 使用 Proxy
拦截读取和设置操作
在 ES2015+ 中,Proxy
提供了一种强大的方式来拦截对象的读取和设置操作。通过 Proxy
,我们可以在读取和设置属性时插入自定义逻辑,从而实现响应式数据。
以下是一个简单的响应式数据实现代码:
// 存储副作用函数的桶
const bucket = new Set();
// 原始数据
const data = { text: 'hello world' };
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 effect 添加到存储副作用函数的桶中
bucket.add(effect);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
bucket.forEach(fn => fn());
// 返回 true 代表设置操作成功
return true;
}
});
3. 测试响应式数据
为了验证我们的响应式数据是否工作,可以编写一个简单的副作用函数,并在数据更新时观察其行为:
// 副作用函数
function effect() {
document.body.innerText = obj.text;
}
// 执行副作用函数,触发读取
effect();
// 1 秒后修改响应式数据
setTimeout(() => {
obj.text = 'hello vue3';
}, 1000);
当运行上述代码时,页面的文本内容会先显示为 "hello world"
,然后在 1 秒后自动更新为 "hello vue3"
。这说明我们的响应式数据系统已经能够正确地记录副作用函数,并在数据更新时触发它们重新执行。
4. 当前实现的局限性
尽管上述代码已经实现了响应式数据的基本功能,但它仍然存在一些明显的局限性:
硬的编码副作用函数
在代码中,我们直接通过effect
函数的名称将其添加到bucket
中。这种硬编码的方式缺乏灵活性。在实际应用中,副作用函数的名称可能是任意的,甚至可能是匿名函数。因此,我们需要一种更通用的方式来动态记录副作用函数。全局副作用函数桶
当前的实现中,所有副作用函数都被存储在一个全局的Set
中。这在单个数据对象的场景下可以工作,但在更复杂的应用中(例如多个响应式对象或嵌套的副作用函数),这种简单的存储方式会导致冲突和错误。缺乏对嵌套副作用函数的支持
当前的实现仅支持单层的副作用函数。在实际应用中,副作用函数可能会嵌套调用,而我们的实现无法正确处理这种嵌套关系。
5. 下一步:改进响应式系统的灵活性和扩展性
在本节中,我们通过 Proxy
实现了一个简单的响应式数据系统,并展示了其基本工作原理。然而,当前的实现还存在许多问题。在接下来的章节中,我们将逐步改进这个系统,使其更加灵活和强大。我们将讨论如何动态记录副作用函数、如何处理嵌套的副作用函数,以及如何优化性能。
通过这一过程,我们将逐步构建一个完整的响应式系统,为实现类似 Vue.js 的数据绑定功能奠定基础。
通过本节的内容,我们不仅理解了响应式数据的基本实现原理,还通过代码示例展示了如何利用 Proxy
拦截对象操作。尽管当前的实现还存在局限性,但它为我们进一步探索响应式系统的设计和优化提供了重要的基础。帮我总结整理归纳一下这个文件中的内容, 并以博客的某一小节的形式输出, 要求内容必须总结输出完整, 内容清晰易懂, 结构明了, 附带上文中的代码, 结合代码来进行总结
构建完善的响应系统:从简单到复杂
在上一节中,我们通过 Proxy
实现了一个简单的响应式数据系统,但它的功能还很有限。在本节中,我们将进一步完善这个系统,使其能够处理更复杂的场景,例如动态注册副作用函数、支持匿名函数,以及正确处理未被副作用函数依赖的属性更新。
1. 响应系统的核心流程
一个完善的响应系统需要实现以下两个核心功能:
收集副作用函数
当读取数据时,将副作用函数收集到一个“桶”中。
例如:function effect(fn) { activeEffect = fn; fn(); }
触发副作用函数的重新执行
当数据更新时,从“桶”中取出与该数据相关的副作用函数并重新执行。
例如:obj.text = "new value"; // 触发副作用函数重新执行
2. 动态注册副作用函数
在上一节的实现中,副作用函数的名字是硬编码的,这限制了系统的灵活性。为了支持动态注册副作用函数,我们引入了一个全局变量 activeEffect
,并通过 effect
函数来注册副作用函数。这样,即使副作用函数是匿名的,也能被正确收集。
let activeEffect;
function effect(fn) {
activeEffect = fn; // 将副作用函数存储到全局变量中
fn(); // 执行副作用函数
}
使用方式如下:
effect(() => {
document.body.innerText = obj.text; // 匿名副作用函数
});
3. 重新设计“桶”的数据结构
在上一节的实现中,我们使用了一个简单的 Set
来存储副作用函数。然而,这种方式无法区分不同属性对应的副作用函数,导致所有副作用函数在任何属性更新时都会被触发。为了解决这个问题,我们需要重新设计“桶”的数据结构。
我们引入了 WeakMap
和 Map
来构建一个更复杂的关系:
WeakMap
:存储目标对象(target
)与属性映射(Map
)的关系。Map
:存储属性名(key
)与副作用函数集合(Set
)的关系。Set
:存储与某个属性相关的副作用函数。
这种结构可以表示为:
target
├── key1
│ ├── effectFn1
│ ├── effectFn2
├── key2
│ ├── effectFn3
代码实现如下:
const bucket = new WeakMap();
function track(target, key) {
if (!activeEffect) return; // 没有激活的副作用函数,直接返回
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect); // 将当前副作用函数添加到依赖集合中
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
effects && effects.forEach(fn => fn()); // 触发所有依赖该属性的副作用函数
}
4. 使用 Proxy
拦截读取和设置操作
结合上述逻辑,我们修改 Proxy
的拦截器,使其在读取操作时调用 track
函数,在设置操作时调用 trigger
函数:
const obj = new Proxy(data, {
get(target, key) {
track(target, key); // 收集副作用函数
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key); // 触发副作用函数重新执行
return true;
}
});
5. 测试和完善
通过上述实现,我们可以测试以下场景:
effect(() => {
console.log("Effect run");
document.body.innerText = obj.text; // 触发收集
});
setTimeout(() => {
obj.text = "Hello Vue3"; // 触发更新
}, 1000);
此外,我们还可以测试以下复杂场景:
effect(() => {
console.log("Effect 1 run");
document.body.innerText = obj.text1;
});
effect(() => {
console.log("Effect 2 run");
document.body.innerText = obj.text2;
});
setTimeout(() => {
obj.text1 = "New Value"; // 只触发与 text1 相关的副作用函数
}, 1000);
6. 使用 WeakMap
的优势
在上述实现中,我们使用了 WeakMap
而不是普通的 Map
。WeakMap
的键是弱引用,不会阻止垃圾回收器回收对象。这意味着,当目标对象(target
)不再被其他变量引用时,它会被自动回收,从而避免内存泄漏。
const map = new Map();
const weakmap = new WeakMap();
(function() {
const foo = { foo: 1 };
const bar = { bar: 2 };
map.set(foo, 1);
weakmap.set(bar, 2);
})();
在上述代码中,foo
会被 map
引用,因此不会被回收;而 bar
会被 weakmap
弱引用,因此可以被垃圾回收器回收。
7. 封装与灵活性
最后,我们将逻辑封装到 track
和 trigger
函数中,这不仅使代码更加清晰,还为后续扩展提供了便利。例如,我们可以在 track
中添加日志记录,在 trigger
中添加防抖或节流功能。
通过本节的改进,我们的响应系统已经能够处理更复杂的场景,支持动态注册副作用函数,并正确区分不同属性的依赖关系。在后续章节中,我们将继续优化这个系统,例如支持嵌套副作用函数、处理数组和对象的嵌套更新等,逐步构建一个功能强大的响应式系统。
分支切换与副作用函数的清理
在响应式系统中,分支切换是一个常见的场景。例如,当一个副作用函数根据数据的不同状态执行不同的代码分支时,可能会导致一些副作用函数的依赖关系变得冗余。如果不正确处理这些冗余的依赖关系,可能会引发不必要的更新。本节将探讨如何解决分支切换带来的副作用函数清理问题。
1. 分支切换的定义与问题
分支切换是指副作用函数在执行时,根据数据的不同状态进入不同的代码分支。例如,以下代码展示了根据 obj.ok
的值切换分支的场景:
const data = { ok: true, text: 'hello world' };
const obj = new Proxy(data, { /* ... */ });
effect(function effectFn() {
document.body.innerText = obj.ok ? obj.text : 'not';
});
在上述代码中,effectFn
根据 obj.ok
的值决定是否读取 obj.text
。当 obj.ok
的值从 true
变为 false
时,原本依赖 obj.text
的副作用函数应该被清理,因为 obj.text
不再被读取。然而,如果不进行清理,obj.text
的更新仍然会触发副作用函数的执行,导致不必要的更新。
2. 副作用函数的清理机制
为了解决分支切换带来的副作用函数清理问题,我们需要在每次副作用函数执行时,先将其从所有依赖集合中移除,然后再重新建立依赖关系。以下是实现这一机制的步骤:
为副作用函数添加依赖集合的引用
在effect
函数中,为副作用函数添加一个deps
属性,用于存储所有包含该副作用函数的依赖集合。let activeEffect; function effect(fn) { const effectFn = () => { cleanup(effectFn); // 清理旧的依赖关系 activeEffect = effectFn; fn(); }; effectFn.deps = []; // 存储依赖集合 effectFn(); }
在
track
函数中收集依赖集合
当副作用函数读取数据时,将依赖集合添加到effectFn.deps
中。function track(target, key) { if (!activeEffect) return; let depsMap = bucket.get(target); if (!depsMap) { bucket.set(target, (depsMap = new Map())); } let deps = depsMap.get(key); if (!deps) { depsMap.set(key, (deps = new Set())); } deps.add(activeEffect); // 将副作用函数添加到依赖集合 activeEffect.deps.push(deps); // 收集依赖集合 }
实现
cleanup
函数
在副作用函数执行前,通过cleanup
函数将其从所有依赖集合中移除。function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i]; deps.delete(effectFn); // 从依赖集合中移除副作用函数 } effectFn.deps.length = 0; // 重置依赖集合数组 }
3. 避免无限循环
在 trigger
函数中,直接遍历依赖集合可能会导致无限循环。这是因为副作用函数在执行时会重新被收集到依赖集合中,而此时遍历尚未结束。为了避免这个问题,我们需要创建一个新的集合来遍历:
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set(effects); // 创建新的集合
effectsToRun.forEach(effectFn => effectFn());
}
4. 完整的代码实现
以下是完整的代码实现:
const bucket = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set(effects);
effectsToRun.forEach(effectFn => effectFn());
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
let activeEffect;
function effect(fn) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
fn();
};
effectFn.deps = [];
effectFn();
}
const data = { ok: true, text: 'hello world' };
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
return true;
}
});
effect(function effectFn() {
document.body.innerText = obj.ok ? obj.text : 'not';
});
const bucket = new WeakMap();
// 用一个全局变量存储被注册的副作用函数
let activeEffect
// 封装get拦截器中的副作用函数收集逻辑
const track = (target, key) => {
if (!activeEffect) return
// 在桶中找target是否存在map key-->effects
let depsMap = bucket.get(target)
// 若不存在depsMap 创建一个新的map
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 在depsMap中找 是否存在key对应的effects
// effect是一个Set结构 用于存储这个key对应的所有副作用函数
let deps = depsMap.get(key)
// 若不存在deps 创建一个新的Set
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 最后将当前激活的副作用函数添加到桶里
deps.add(activeEffect)
// --处理activeEffect.deps
// deps就是一个与当前副作用函数存在联系的依赖集合
// 将deps添加到activeEffect.deps中 所以activeEffect.deps是一个存储set的数组 Set[]
activeEffect.deps.push(deps)
}
// 封装set拦截器中副作用函数调用逻辑
const trigger = (target, key) => {
// 先从桶中找到对应的target的map
const depsMap = bucket.get(target)
if (!depsMap) return;
// 再从depsMap中找到对应的key的set
const deps = depsMap.get(key)
// 声明一个新的deps来clone deps 防止在执行deps中的副作用函数时, deps集合发生变化 导致死循环
const depsClone = new Set(deps)
// 执行deps中的副作用函数
depsClone && depsClone.forEach(fn => fn())
}
// cleanup函数用于清除副作用函数
const cleanup = (effectFn) => {
// 遍历effectFn.deps数组
for(let i = 0; i < effectFn.deps.length; i++) {
// deps是一个Set结构 依赖集合
const deps = effectFn.deps[i]
// 将effectFn从依赖集合中删除
deps.delete(effectFn)
}
// 最后需要重置effectFn.deps数组
effectFn.deps.length = 0
}
// effect函数用于<注册>副作用函数 (解决依赖函数名的问题)
const effect = (fn) => {
const effectFn = () => {
// 调用cleanup函数完成清除工作
cleanup(effectFn)
// 当effectFn执行时, 将其设置为当前激活的副作用函数
activeEffect = effectFn
// 执行fn
fn()
}
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
const data = { ok: true, text: "hello world" }
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
console.log(key + ' deps:', bucket)
// 返回属性值
return target[key]
},
set(target, key, newVal) {
console.log('set run key:', key)
target[key] = newVal
trigger(target, key)
}
})
// 直接通过匿名函数注册一个副作用函数
effect(function effectFn() {
console.log("effect run")
// 读取的时候会触发obj的get
document.body.innerText = obj.ok? obj.text: 'not'
})
setTimeout(() => {
// 改变obj的text属性的值
// obj.text = "hello vue"
obj.ok = false
setTimeout(() => {
obj.text = "hello vue"
}, 2000)
// 若改变一个不存在的属性的值
// obj.notExist = "hello vue"
}, 1000)
5. 测试与验证
以下是测试代码:
obj.ok = false; // 分支切换,触发副作用函数重新执行
obj.text = 'hello vue3'; // 不应触发副作用函数,因为 obj.text 不再被依赖
在上述测试中,当 obj.ok
的值变为 false
时,副作用函数会重新执行,但此时不再依赖 obj.text
。因此,后续修改 obj.text
不会触发副作用函数的执行。
6. 总结
通过引入 cleanup
函数和依赖集合的引用,我们成功解决了分支切换带来的副作用函数清理问题。同时,通过在 trigger
函数中创建新的集合来避免无限循环,确保了响应式系统的稳定运行。这一改进使得我们的响应式系统更加健壮,能够正确处理复杂的依赖关系和分支切换场景。
嵌套的 Effect 与 Effect 栈
在响应式系统中,effect
函数用于注册副作用函数,而副作用函数可能会嵌套调用其他 effect
。例如,在 Vue.js 中,组件的渲染函数通常被包裹在一个 effect
中执行,而组件嵌套会导致 effect
的嵌套。本节将探讨如何支持嵌套的 effect
,并解决嵌套带来的问题。
1. 嵌套 Effect 的场景
在实际开发中,嵌套的 effect
是常见的。例如,当一个组件渲染另一个组件时,就会产生嵌套的副作用函数。以下是一个简单的示例:
effect(function effectFn1() {
console.log("effectFn1 执行");
effect(function effectFn2() {
console.log("effectFn2 执行");
});
});
在这个例子中,effectFn1
内部嵌套了 effectFn2
,并且 effectFn2
的执行依赖于 effectFn1
的执行。
2. 不支持嵌套时的问题
在之前的实现中,effect
函数使用了一个全局变量 activeEffect
来存储当前激活的副作用函数。然而,这种实现方式不支持嵌套的 effect
,因为全局变量 activeEffect
在嵌套调用时会被覆盖。例如:
const data = { foo: true, bar: true };
const obj = new Proxy(data, { /* ... */ });
let temp1, temp2;
effect(function effectFn1() {
console.log("effectFn1 执行");
effect(function effectFn2() {
console.log("effectFn2 执行");
temp2 = obj.bar; // 读取 obj.bar
});
temp1 = obj.foo; // 读取 obj.foo
});
在上述代码中,effectFn2
的执行会覆盖 activeEffect
的值。当修改 obj.foo
时,我们希望触发 effectFn1
的重新执行,但实际上却触发了 effectFn2
的执行。这是因为 activeEffect
被覆盖后,obj.foo
的依赖关系被错误地绑定到了 effectFn2
。
3. 使用 Effect 栈解决嵌套问题
为了解决嵌套 effect
的问题,我们需要引入一个副作用函数栈(effectStack
)。通过将当前执行的副作用函数压入栈中,并在执行完毕后弹出栈,我们可以确保 activeEffect
始终指向栈顶的副作用函数。以下是改进后的代码:
// 用一个全局变量存储当前激活的 effect 函数
let activeEffect;
// effect 栈
const effectStack = [];
function effect(fn) {
const effectFn = () => {
cleanup(effectFn); // 清理旧的依赖关系
try {
// 将当前副作用函数压入栈中
effectStack.push(effectFn);
activeEffect = effectFn;
fn(); // 执行副作用函数
} finally {
// 执行完毕后,将当前副作用函数弹出栈
effectStack.pop();
// 恢复之前的 activeEffect
activeEffect = effectStack[effectStack.length - 1];
}
};
effectFn.deps = []; // 存储依赖集合
effectFn();
}
通过这种方式,我们确保了嵌套的 effect
能够正确处理依赖关系。当内层的 effect
执行完毕后,activeEffect
会恢复为外层的 effect
,从而避免了依赖关系的混乱。
4. 测试与验证
以下是测试代码:
effect(function effectFn1() {
console.log("effectFn1 执行");
effect(function effectFn2() {
console.log("effectFn2 执行");
temp2 = obj.bar; // 读取 obj.bar
});
temp1 = obj.foo; // 读取 obj.foo
});
obj.foo = false; // 触发 effectFn1 重新执行
obj.bar = false; // 触发 effectFn2 重新执行
在上述测试中,修改 obj.foo
时,只触发了 effectFn1
的重新执行;修改 obj.bar
时,只触发了 effectFn2
的重新执行。这表明嵌套的 effect
已经能够正确处理依赖关系。
5. 总结
通过引入 effectStack
,我们解决了嵌套 effect
的问题,使得响应式系统能够正确处理嵌套的副作用函数。这种实现方式不仅支持复杂的嵌套场景,还确保了依赖关系的准确性。在实际开发中,这种机制对于组件嵌套和复杂的数据依赖关系尤为重要。
避免无限递归循环:响应式系统中的陷阱与解决方案
在构建响应式系统时,无限递归循环是一个常见的问题。这种问题通常发生在副作用函数中同时包含读取和设置操作时。本节将探讨无限递归循环的成因,并提供一种有效的解决方案。
1. 无限递归循环的成因
假设我们有以下代码:
const data = { foo: 1 };
const obj = new Proxy(data, { /*...*/ });
effect(() => {
obj.foo++; // 自增操作
});
在这段代码中,effect
注册的副作用函数中包含了一个自增操作 obj.foo++
。这个操作实际上可以拆解为两步:
effect(() => {
obj.foo = obj.foo + 1; // 读取并设置
});
问题出在 obj.foo
的读取和设置操作上:
当副作用函数执行时,
obj.foo
的读取操作会触发track
,将副作用函数收集到依赖集合中。随后,
obj.foo
的设置操作会触发trigger
,导致依赖集合中的副作用函数重新执行。由于副作用函数尚未执行完毕,
trigger
又会触发副作用函数的重新执行,从而导致无限递归。
这种无限递归最终会导致栈溢出错误:
Uncaught RangeError: Maximum call stack size exceeded
2. 解决无限递归循环的方法
要解决这个问题,我们需要在 trigger
函数中增加一个条件判断:如果触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。以下是改进后的 trigger
函数实现:
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set();
effects && effects.forEach(effectFn => {
// 如果触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
effectsToRun.forEach(effectFn => effectFn());
}
通过这种方式,我们避免了副作用函数在执行过程中触发自身的重新执行,从而解决了无限递归的问题。
3. 总结
在响应式系统中,无限递归循环是一个常见的问题,尤其是在副作用函数中同时包含读取和设置操作时。通过在 trigger
函数中增加条件判断,避免触发自身重新执行,我们可以有效解决这一问题。这种解决方案不仅简单高效,还能确保响应式系统的稳定运行。