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

我是时橙,下次再见。

相关推荐
续亮~1 小时前
6、Redis系统-数据结构-05-整数
java·前端·数据结构·redis·算法
顶顶年华正版软件官方2 小时前
剪辑抽帧技巧有哪些 剪辑抽帧怎么做视频 剪辑抽帧补帧怎么操作 剪辑抽帧有什么用 视频剪辑哪个软件好用在哪里学
前端·音视频·视频·会声会影·视频剪辑软件·视频剪辑教程·剪辑抽帧技巧
程序员云翼3 小时前
7-理财平台
java·vue.js·spring boot·后端·毕设
托尼沙滩裤3 小时前
【js面试题】js的数据结构
前端·javascript·数据结构
不熬夜的臭宝3 小时前
每天10个vue面试题(一)
前端·vue.js·面试
朝阳394 小时前
vue3【实战】来回拖拽放置图片
javascript·vue.js
不如喫茶去4 小时前
VUE自定义新增、复制、删除dom元素
前端·javascript·vue.js
长而不宰4 小时前
vue3+electron项目搭建,遇到的坑
前端·vue.js·electron
阿垚啊4 小时前
vue事件参数
前端·javascript·vue.js
加仑小铁4 小时前
【区分vue2和vue3下的element UI Dialog 对话框组件,分别详细介绍属性,事件,方法如何使用,并举例】
javascript·vue.js·ui