昨天我们解决了单一依赖所导致的指数增长问题。然而,在真实的开发场景中,一个 effect 函数往往需要依赖多个响应式变量。现在我们试着新增多个依赖,在示例中加入第二个响应式变量 count,并让 effect 同时依赖 flag 和 count。按钮的点击事件只会修改 count 的值。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
<style>
body {
padding: 150px;
}
</style>
</head>
<body>
<button id="btn">按钮</button>
<script type="module">
import { ref, effect } from '../dist/reactivity.esm.js'
const flag = ref(true)
const count = ref(0)
effect(() => {
flag.value
count.value
console.count('effect')
})
btn.onclick = () => {
count.value++
}
</script>
</body>
</html>
当
effect 函数同时依赖 flag 和 count 两个响应式变量后,点击按钮触发更新时,问题又回来了,依赖收集再次出现了指数级的增长。为什么会这样?
初始化
页面第一次加载,
effect 执行一次。它依次读取 flag.value 和 count.value,触发两次依赖收集。
flag通过Link1与effect关联。count通过Link2与effect关联。effect自己的deps链表也记录了Link1 -> Link2。
到目前为止,都没什么问题。
第一次点击按钮
当按钮被点击,count.value++ 触发更新,effect 的 run() 方法被调用。
- 进入
run():depsTail被设为undefined。 deps仍然指向Link1。
(deps && !depsTail) 的状态,就是我们用来判断是否“正在重新执行”的关键点。
-
读取
flag.value,触发第一次link()-
effect函数体重新执行,先读取flag.value,触发link(flag, effect)。 -
link()开始判断:- 条件
(sub.deps && !sub.depsTail)成立。 - 接着检查
sub.deps.dep === dep(即Link1的dep是否为flag),成立。
- 条件
-
复用成功,
depsTail移动到了flag对应的Link1节点上。
-
-
读取
count.value,触发第二次link()-
effect继续执行,读取count.value,触发link(count, effect)。 -
link()再次判断:- 条件
(sub.deps && !sub.depsTail)不成立,因为刚刚depsTail已经被赋值,不再是undefined。 - 复用检查直接失败,创建了一个全新的
Link3节点,并移动了指针。
- 条件
-
执行完毕后,
flag 的依赖 (Link1) 被正确复用,但 count 的依赖被重复新增了节点 (Link2 依然存在,又新增了 Link3)。
这导致下一次点击按钮时,count 的 propagate 就会触发 effect 执行两次(通过 Link2 和 Link3),因此造成了指数增长。
问题分析:为何只有第一个依赖被正确复用?
逻辑漏洞在于:
- 我们的复用检查
if (sub.deps.dep === dep)只检查了sub.deps(头节点),它永远只拿effect依赖链表的第一个节点来比较,导致只有第一个依赖能被成功复用。 - 一旦第一个依赖复用成功,
depsTail就被赋值,导致后续所有依赖的复用检查条件(sub.deps && !sub.depsTail)直接失败。
今天我们通过图解,一步步追踪了内部依赖链表的情况。分析后可以知道,问题的根源在于现有的节点复用逻辑存在漏洞:它只会检查并比对依赖链表的第一个节点 (sub.deps)。
明天我们将基于这次的问题解析结果,来着手实现解决方案。
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。