从零到一打造 Vue3 响应式系统 Day 12 - Effect :多重依赖之指数触发重现

363 阅读3分钟

ZuB1M1H.png

昨天我们解决了单一依赖所导致的指数增长问题。然而,在真实的开发场景中,一个 effect 函数往往需要依赖多个响应式变量。现在我们试着新增多个依赖,在示例中加入第二个响应式变量 count,并让 effect 同时依赖 flagcount。按钮的点击事件只会修改 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>

day12-01.gifeffect 函数同时依赖 flagcount 两个响应式变量后,点击按钮触发更新时,问题又回来了,依赖收集再次出现了指数级的增长。为什么会这样?

初始化

day12-02.png 页面第一次加载,effect 执行一次。它依次读取 flag.valuecount.value,触发两次依赖收集。

  • flag 通过 Link1effect 关联。
  • count 通过 Link2effect 关联。
  • effect 自己的 deps 链表也记录了 Link1 -> Link2

到目前为止,都没什么问题。

第一次点击按钮

day12-03.png

当按钮被点击,count.value++ 触发更新,effectrun() 方法被调用。

  • 进入 run()depsTail 被设为 undefined
  • deps 仍然指向 Link1

(deps && !depsTail) 的状态,就是我们用来判断是否“正在重新执行”的关键点。

day12-04.png

  • 读取 flag.value,触发第一次 link()

    • effect 函数体重新执行,先读取 flag.value,触发 link(flag, effect)

    • link() 开始判断:

      • 条件 (sub.deps && !sub.depsTail) 成立
      • 接着检查 sub.deps.dep === dep (即 Link1dep 是否为 flag),成立
    • 复用成功,depsTail 移动到了 flag 对应的 Link1 节点上。

day12-05.png

  • 读取 count.value,触发第二次 link()

    • effect 继续执行,读取 count.value,触发 link(count, effect)

    • link() 再次判断:

      • 条件 (sub.deps && !sub.depsTail) 不成立,因为刚刚 depsTail 已经被赋值,不再是 undefined
      • 复用检查直接失败,创建了一个全新的 Link3 节点,并移动了指针。

day12-06.png 执行完毕后,flag 的依赖 (Link1) 被正确复用,但 count 的依赖被重复新增了节点 (Link2 依然存在,又新增了 Link3)。

这导致下一次点击按钮时,countpropagate 就会触发 effect 执行两次(通过 Link2Link3),因此造成了指数增长。

问题分析:为何只有第一个依赖被正确复用?

逻辑漏洞在于:

  • 我们的复用检查 if (sub.deps.dep === dep) 只检查了 sub.deps(头节点),它永远只拿 effect 依赖链表的第一个节点来比较,导致只有第一个依赖能被成功复用。
  • 一旦第一个依赖复用成功,depsTail 就被赋值,导致后续所有依赖的复用检查条件 (sub.deps && !sub.depsTail) 直接失败。

今天我们通过图解,一步步追踪了内部依赖链表的情况。分析后可以知道,问题的根源在于现有的节点复用逻辑存在漏洞:它只会检查并比对依赖链表的第一个节点 (sub.deps)。

明天我们将基于这次的问题解析结果,来着手实现解决方案。


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