Vue3.4黑魔法:双链表重构后的新响应式系统

写在前面

大家好,我是时橙~

本文为新式响应式系统的解析,该pr已被正式合并入minor分支,如果解析有不到位的地方,望各位提表建议。

本次重构来源于尤雨溪的本次pr ⬇️

github.com/vuejs/core/...

新·响应式系统带来的收益

  • 解决了几个bugs #10290 #10069
  • 更高效的内存使用,基准测试提升56%
  • 更合理的computed行为
  • ....

正文

现在的订阅者(Subscriber)和依赖(dep)的抽象关系长什么样?

现在看不懂这个图谱没有关系,后续我们再解释这个表式什么意思

Link是什么东西?

3.4版本内部的Link接口定义如下:

ts 复制代码
interface Link {
  dep: Dep
  sub: Subscriber

  /**
   * - Before each effect run, all previous dep links' version are reset to -1
   * - During the run, a link's version is synced with the source dep on access
   * - After the run, links with version -1 (that were never used) are cleaned
   *   up
   */
  version: number //依赖每次被触发时+1

  /**
   * Pointers for doubly-linked lists
   */
  nextDep?: Link
  prevDep?: Link

  nextSub?: Link
  prevSub?: Link

  prevActiveLink?: Link
 }

归纳一下:

  • 承载依赖(Dep)和订阅者(Sub)
  • 版本(version),版本意味着dep被访问的次数
  • 四方链表怪兽😂,就正如上图中的这个节点,它可以指向四个方向

Link在依赖的追踪(dep.track)和依赖的触发(dep.trigger)中起着至关重要的作用,不过让我们重温依赖追踪/触发之前,我们再看看重看链表图,理解一下到底怎么看这个链表图

再看链表图

我们链表图看成一个坐标图,它有Dep轴和Sub轴,每个Link代表一个坐标节点,我在下面这张图标记了坐标轴和Link节点序号

举例:在整个图谱绘制完之后,

  • Link 1 上的订阅者(sub)和依赖(dep)分别是Dep1和Sub1.
  • Link 5 上的订阅者(sub)和依赖(dep)分别是Dep2和Sub2.
  • Link 6 上的订阅者(sub)和依赖(dep)分别是Dep3和Sub2.

PS: 每个dep还会指向队尾Link,而每个sub会有两个箭头,一个指向队头,一个指向队尾。

那么,现在有两个问题,

  • 这个图谱是怎么绘制出来的?
  • 我们如何根据这个图谱,在依赖(dep)更新时,让所有订阅者(sub)进行更新?

图谱的绘制

9x9的宫格实在是太大了😂 我们换一个4x4的图谱来看看它怎么绘制的 图谱版本: 代码版本:

typescript 复制代码
    let dummy1, dummy2
    //dep1
    const counter1 = reactive({ num: 1 })
    //dep2
    const counter2 = reactive({ num: 2 })
    //sub1
    effect(() => {
      dummy1 = counter1.num + counter2.num 
    })
    //sub2
    effect(() => {
      dummy2 = counter1.num + counter2.num + 1
    })

    expect(dummy1).toBe(3)
    expect(dummy2).toBe(4)
    counter1.num++
    counter2.num++
    expect(dummy1).toBe(5)
    expect(dummy2).toBe(6)

让我们一起回顾一下如何绘制出这个图谱:

左右建联

第一个sub1在第7行创建

ts 复制代码
    effect(() => {
      dummy1 = counter1.num + counter2.num 
    })

然后sub1会在effect执行的时候创建此时执行到第七行我们的图表是这样的: sub1会被赋予在全局activeSub上,然后effect运行回调

typescript 复制代码
 dummy1 = counter1.num + counter2.num 

此时counter1会创建只属于它的Dep1

然后便会进行依赖追踪dep1.track来追踪全局上的ActiveSub(sub1)。

然鹅,dep1和sub1要如何联系起来?我们偷窥一眼源码(只看红框即可):

link就这么顺理成章的把dep1和sub1都联系起来了! 现在我们的图谱长这样:

不过这只是以link视角的 完整一次track建立完之后的图谱应该是,:

大家在这张图中只需要知道订阅者Sub1会追踪它关联依赖的Link的队头和队尾(这里暂时是Link1)

依赖Dep1会追踪它关联订阅者的Link(这里是Sub1)

我们可以得到一个启示:

Link在依赖Dep视角是订阅节点,而在订阅者Sub视角是依赖节点。

Link同时关联依赖和订阅者,我称之为依赖/订阅的二象性(笑)

后续我们将不再展示完整图谱,因为这样图谱会太复杂了,大家只需要知道Sub能知晓依赖链的头尾,Dep能知道Sub链的尾就行。

接下来,回到回调函数会执行counter2.num 依然如dep1创建时一样,不过link1和link2是如何联系起来的呢?

我们依然继续偷窥源码(只看画红框的部分就好了):

看else里的逻辑,我们只需要取订阅者队尾然后链接上即可!

上下建联

不过link的上下建联的逻辑是怎么做的?

还是在依赖追踪的逻辑中,在左右建联完之后就会进行上下建联:addSub(link)。

上下建联的原理类似于左右建联,不过这次是dep为主导,依然是链表操作:⬇️

typescript 复制代码
function addSub(link: Link) {
  const currentTail = link.dep.subs
  if (currentTail !== link) {
    link.prevSub = currentTail
    if (currentTail) currentTail.nextSub = link
  }
  link.dep.subs = link
}

至此,我们的蓝图已经完成了!

有了这样的一张图表,我们能做什么?

优化内存性能1 - 在依赖追踪和触发时:复用Link节点

假如现在有这么一串代码:

typescript 复制代码
const dep1 = reactive({value:true});
const dep2 = reactive({value:1});
const dep3 = reactive({value:1});
const dep4 = reactive({value:1});
effect(() => {
  if (dep1.value) {
    dep2.value;
    dep3.value;
  } else {
    dep3.value;
    dep4.value;
  }
});

第一次运行依赖时他的图谱如下⬇️

如果此时设置dep1.value=false

那么新收集的依赖便是 在这个过程中,

  • 在触发结束阶段,清理无效依赖时,我们通过Dep上的属性version判断Dep是否被访问,然后将Link3放逐掉,悬空的Link对V8的垃圾回收非常友好,提升内存回收性能。
  • 在追踪时:Link1,Link2仍然被继续复用,只创建了Link4,减小了内存创建开销。

优化运行性能2 - 减少依赖调用堆栈

在这个蓝图下如果任意一个link的dep被触发了,我们直接沿着链表从上到下依次触发sub1,sub2,sub3即可。 如果是曾经的vue3,可能会存在某些递归调用,创建递归堆栈是一种非常消耗内存的行为。

魔法Computed-缓存比较

如果有一个Computed更新前后的值无所改变,那么它会直接中断订阅Computed的订阅者的更新。 假设有这么一个图谱, 对应代码如下:

ts 复制代码
const dep2=ref(0)
const v=computed(()=>{
  return dep2.value % 2
)

effect(()=>{
  v
))
effect(()=>{
  v
  dep2
))

此时如果更新:Dep2.value=2

那么由于缓存比较,

由于Sub1订阅了v,v没有改变,所以不会运行,

由于Sub2订阅了v合dep2,dep2改变了,effect会被再次运行。

当然如果是链式的computed,在缓存策略下能避免更新整个订阅链,订阅链会根据computed缓存在合适的情况下中途终止。

当然本次更新还给Coumpted带来了更多的特性,但本人精力有限,剩下的就留给各位探索啦~

我是时橙,下次再见。

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax