从零到一打造 Vue3 响应式系统 Day 10 - 为何 Effect 会被指数级触发?
DOM 交互
我们的响应式系统经过前几天的努力,已经初具雏形,感觉可以加入一些 DOM 交互,来进行简单的测试。
<!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)
effect(() => {
flag.value
console.count('effect')
})
btn.onclick = () => {
flag.value = !flag.value
}
</script>
</body>
</html>
我们预期每次点击按钮,effect
只会执行一次。但实际情况看起来不太妙。
从 console.count
的结果可以看到,effect
的执行次数随着点击呈现指数级增长。这肯定是不行的。
我们来了解一下问题的症结所在。
Link 节点创建问题的症结
执行步骤图解
初始化页面
页面加载时,
effect
执行一次。在执行过程中,读取了 flag.value
,触发 getter 进行依赖收集。 系统会创建一个 link1
节点,将 effect
与 flag
关联起来。到这里都符合预期。
第一次点击按钮
当按钮第一次被点击,
flag.value
从 true
变为 false
,触发了 setter。 setter 内的 propagate
函数开始遍历 flag
的依赖链表。
propagate
执行 link1
中存储的 effect.run()
。
effect
函数重新执行,又读取了 flag.value
,再次触发了 getter。
此时问题出现了:在 effect.run()
的过程中,又进行了一次依赖收集,系统创建了一个新的 link2
节点并添加到链表尾部。
执行结束后的链表:
第二次点击按钮
当按钮又被点击,
flag.value
从 false
变为 true
,再次触发 setter。
propagate
开始遍历依赖链表。但这一次,链表上有两个节点 (link1
和 link2
)。
-
propagate
先执行link1
中的effect.run()
。effect
内部读取flag.value
,触发依赖收集,创建了一个新的link3
节点并添加到链表尾部。 -
propagate
接着执行link2
中的effect.run()
。effect
内部又一次读取flag.value
,触发依赖收集,又创建了一个新的link4
节点并添加到链表尾部。
执行结束后的链表:
执行完成后的链表结构
我们可以发现在触发更新时,链表上的每一个节点都会触发一次 effect
的重新执行,而每一次执行又会创建一个新的节点加入到链表中,因此发生了指数级触发 effect
的情况。
关键问题点
每次 effect
重新执行时:
- 没有检查该
effect
是否已经存在于依赖链表中。 - 盲目地创建新的
Link
节点并添加到链表末尾。 - 导致依赖链表在每次更新时都会成倍增长。
因此,每次点击按钮,链表上的每一个 Link
都会触发一次 effect
的重新执行,而在每一次执行中又会创建新的 Link
,从而导致重复执行和指数级增长现象。
因为下个篇幅比较长,今天就先讲到这里。大家需要先理解问题的症结所在,这样明天在实现解决方案时,才能明白我们为什么要那样做。
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。