阅读视图

发现新文章,点击刷新页面。

从零到一打造 Vue3 响应式系统 Day 10 - 为何 Effect 会被指数级触发?

ZuB1M1H.png

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>

day10-01.gif

我们预期每次点击按钮,effect 只会执行一次。但实际情况看起来不太妙。

console.count 的结果可以看到,effect 的执行次数随着点击呈现指数级增长。这肯定是不行的。

我们来了解一下问题的症结所在。

Link 节点创建问题的症结

执行步骤图解

初始化页面

day10-06.png 页面加载时,effect 执行一次。在执行过程中,读取了 flag.value,触发 getter 进行依赖收集。 系统会创建一个 link1 节点,将 effectflag 关联起来。到这里都符合预期。

第一次点击按钮

day10-02.png 当按钮第一次被点击,flag.valuetrue 变为 false,触发了 setter。 setter 内的 propagate 函数开始遍历 flag 的依赖链表。

propagate 执行 link1 中存储的 effect.run()

effect 函数重新执行,又读取了 flag.value,再次触发了 getter。

此时问题出现了:在 effect.run() 的过程中,又进行了一次依赖收集,系统创建了一个新的 link2 节点并添加到链表尾部。

执行结束后的链表:

day10-03.png

第二次点击按钮

day10-04.png 当按钮又被点击,flag.valuefalse 变为 true,再次触发 setter。

propagate 开始遍历依赖链表。但这一次,链表上有两个节点 (link1link2)。

  1. propagate 先执行 link1 中的 effect.run()effect 内部读取 flag.value,触发依赖收集,创建了一个新的 link3 节点并添加到链表尾部。
  2. propagate 接着执行 link2 中的 effect.run()effect 内部又一次读取 flag.value,触发依赖收集,又创建了一个新的 link4 节点并添加到链表尾部。

执行结束后的链表:

day10-05.png

执行完成后的链表结构

我们可以发现在触发更新时,链表上的每一个节点都会触发一次 effect 的重新执行,而每一次执行又会创建一个新的节点加入到链表中,因此发生了指数级触发 effect 的情况。

关键问题点

每次 effect 重新执行时:

  1. 没有检查该 effect 是否已经存在于依赖链表中。
  2. 盲目地创建新的 Link 节点并添加到链表末尾。
  3. 导致依赖链表在每次更新时都会成倍增长。

因此,每次点击按钮,链表上的每一个 Link 都会触发一次 effect 的重新执行,而在每一次执行中又会创建新的 Link,从而导致重复执行和指数级增长现象。

因为下个篇幅比较长,今天就先讲到这里。大家需要先理解问题的症结所在,这样明天在实现解决方案时,才能明白我们为什么要那样做。


想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

❌