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带来了更多的特性,但本人精力有限,剩下的就留给各位探索啦~

我是时橙,下次再见。

相关推荐
别拿曾经看以后~6 分钟前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死9 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人20 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人21 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR26 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香28 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q24985969331 分钟前
前端预览word、excel、ppt
前端·word·excel
小华同学ai36 分钟前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍