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 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder1 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_882727571 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
SoaringHeart1 小时前
Flutter进阶:基于 MLKit 的 OCR 文字识别
前端·flutter
会发光的猪。2 小时前
css使用弹性盒,让每个子元素平均等分父元素的4/1大小
前端·javascript·vue.js
天下代码客2 小时前
【vue】vue中.sync修饰符如何使用--详细代码对比
前端·javascript·vue.js
猫爪笔记2 小时前
前端:HTML (学习笔记)【1】
前端·笔记·学习·html
前端李易安2 小时前
Webpack 热更新(HMR)详解:原理与实现
前端·webpack·node.js
红绿鲤鱼2 小时前
React-自定义Hook与逻辑共享
前端·react.js·前端框架
周全全2 小时前
Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
java·vue.js·spring boot·安全·php