昨天我们解决了单一依赖所导致的指数增长问题。然而,在真实的开发场景中,一个 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」,一起跟日安当同学。