ZuB1M1H.png

在解决了链表节点指数增长的问题后,我们还需要关注依赖的有效性。

effect 的执行路径可能因为条件判断或程序逻辑不同而改变,导致某些依赖在本次执行中已经不再需要。

如果这些“过期依赖”没有被清理:

  • 会造成 内存泄漏:不需要的链表节点一直被保留。
  • 会导致 不必要的更新effect 虽然已经不依赖某个 ref,但这个 ref 的变化仍然会触发 effect
  • 会引发 性能下降:随着时间累积,无效链表节点越来越多,增加整体执行成本。

因此,在收集依赖时,不只要能正确复用,也必须具备 清理过期依赖 的机制。

下面我们来说明需要清理依赖的两种状况。

场景一:条件型依赖

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <style>
      body {
        padding: 150px;
      }
    </style>
  </head>
<body>
  <div id="app"></div>
  <button id="flagBtn">update flag</button>
  <button id="nameBtn">update name</button>
  <button id="ageBtn">update age</button>
  <script type="module">
    // import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { ref, effect } from '../dist/reactivity.esm.js'

    const flag = ref(true)
    const name = ref('姓名')
    const age = ref(18)

    effect(() => {
      console.count('effect')

      if(flag.value){
        app.innerHTML = name.value
      } else {
        app.innerHTML = age.value
      }
    })

    flagBtn.onclick = () => {
      flag.value = !flag.value
    }
    nameBtn.onclick = () => {
      name.value = '姓名' + Math.random()
    }
    ageBtn.onclick = () => {
      age.value++
    }
  </script>
</body>
</html>

我们现在有三个变量,flagnameage。预期行为是:

  • 如果 flagtrue,那么点击 name 按钮会触发更新,但是点击 age 按钮不会触发更新。
  • 如果 flagfalse,那么点击 age 按钮会触发更新,但是点击 name 按钮不会触发更新。

这很合理,因为在某个分支下未被依赖的变量,其变化不应该触发更新。

实际情况

day14-01.gif

  • 初始化:输出 effect: 1,此时 flagtrue
  • 点击 update flageffect 更新,输出 effect: 2,此时 flag 变为 false
  • 点击 update name:我们期望它没有反应,因为“如果 flagfalse,点击 name 不会触发更新。”
  • effect 仍然被更新,console 输出了 effect: 3

问题说明

我们先来看一下 effect 以及 name 内部的内容。执行步骤是初始化后先点击 update flag,再点击 update name

检查 Effect 返回值

const e = effect(() => {
  console.count('effect')
  if(flag.value){
    app.innerHTML = name.value
  } else {
    app.innerHTML = age.value
  }
})
//...
nameBtn.onclick = () => {
  name.value = '姓名' + Math.random()
  console.dir(e.effect) // 访问挂载在 runner 上的 effect 实例
  console.log(name)
}

day14-02.png 我们预期 effect 内部的依赖链表 (deps) 中,不会有 name 节点:实际上是正确的,当前 Link 节点是 flagage

day14-03.png 我们预期 name 的订阅者链表 (subs) 中,不应该有 effect 的记录,但实际上输出结果显示它仍处于被订阅的状态。

异常情况分析

我们现在了解到异常情况是:

  • effect 内部的依赖链表上只有 flagage,没有 name,这是我们想要的正确结果。
  • name 内部的订阅者链表上却仍然记录着这个 effect,这层关系应该被清除。

Effect 依赖清理问题图解说明

页面初始化

day14-04.png 遇到 flag,收集依赖,创建链表节点。

day14-05.png 遇到 name,创建链表节点。此时依赖关系是:effect 依赖 flagname

点击按钮,触发更新

day14-06.png 点击按钮,effect 重新执行,run 函数让 depsTail 指向 undefined,进入“重新收集”状态。

day14-07.png 接着 link 函数检查 flag,发现可以复用 Link1

day14-08.png depsTail 指向 Link1,此时 flag 的值是 false

day14-09.png 由于 flagfalse,程序进入 else 分支,遇到 age。之前没有收集过 age 的依赖,于是创建新的链表节点 Link3

day14-10.png 创建 age 的链表节点之后,effectdeps 链表更新,depsTail 指向新节点 Link3

day14-11.png 问题来了:此时请看黄底色的地方,你会发现 effectdeps 链表上已经没有 Link2 (连接 name 的节点) 了,但是 Link2sub 属性仍然指向这个 effect。同时,namesubs 链表也依然保留着 Link2

所以才会出现 name 更新时,仍然会触发 effect 执行的情况。

场景二:提前返回 (Early Return)

const flag = ref(true)
const name = ref('姓名')
const age = ref(18)
let count = 0

effect(() => {
  console.count('effect')

  if(count > 0) return
  count++

  if(flag.value){
    app.innerHTML = name.value
  } else {
    app.innerHTML = age.value
  }
})
// ... (按钮点击事件同上)

到时修正时会一并处理,所以我们先来说明第二种需要清除依赖的状况。

预期effect 只会触发两次。(因为 count > 0 会返回,之后无论如何点击按钮都不应再触发)

初始化

  • 此时 count0,继续往下执行。

    • count++count 现在是 1
    • 读到 flag.value → 收集依赖 flag
    • flagtrue,接着读到 name.value → 收集依赖 name

day14-12.png

  • 点击 name 按钮
    • 触发 effect 重新 run()
    • depsTail 设为 undefined
    • 进入 effect 函数体,马上遇到 if (count > 0) return 而被中断,因此本次执行没有进行任何依赖收集

遇到 return 程序应该会中断,可是你一直点击 name 按钮,console.count('effect') 仍然一直触发更新。 day14-13.png 当遇到 if (count > 0) return 时,effect 没有访问任何依赖,其 deps 链表上应该要是空的。可是,链表上面有之前建立的依赖关系没有被清除。

此时 effectdeps 链表状态一直处于:有头节点 deps,并且尾节点 depsTail = undefined

明天我们就会来谈如何清除这些过期的依赖。


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

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]