a house in the woods

Hi, nice to meet you.

  1. 1. 前言
  2. 2. 本文
    1. 2.1. 数据结构
    2. 2.2. 依赖追踪
    3. 2.3. 通知去重
    4. 2.4. 批量更新
    5. 2.5. 其他
  3. 3. 后记

前言

最新的一期Javascript Weekly记载Vue 3.5发布了,其中第一点改动提到了重构它reactity的实现。而这个实现的灵感来自于preactsignal,使用了doubly linked-listversion number,而它的核心源码只有一个文件,算是比较易读的。

本文

数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Node = {
_source: Signal,
_prevSource?: Node,
_nextSource?: Node,

_target: Computed | Effect,
_prevTarget?: Node,
_nextTarget?: Node
_version: number
}

class Signal {
_version: number
_node?: Node
_targets?: Node
}

class Computed extends Signal {
_sources?: Node
}

class Effect {
_sources?: Node
_nextBatchedEffect?: Effect
}

source表示依赖项,target表示订阅者,sources通常存的是依赖链,是正序的,而targets存的是订阅链,跟倒序的。targets倒序可能是因为batched effect是先进后出的,这样子从末尾开始notify可以保证effect按正向执行。

依赖追踪

初始时,首先会在Computed/Effect执行callback前设自己为全局的evalContext,在callback执行时如果get signalvalue,则会创建一个node, 其source设为这个signaltarget设为这个Computed/Effect,再把这个node加入到Computed/Effect的依赖链中以及signal的订阅链中。

随后,当某些source发生变化时,会执行这些订阅者的callback。而此时,订阅者会执行prepareSources这个步骤,目的是通过将依赖链的所有nodeversion置为-1,标记上次的依赖项,当callback执行完,在cleanupSources这一步中,把version依然是-1node剔除出依赖链;prepareSources除上述的作用以外,还会把node设到source._node里,_node其实是跟当前evalContext关联的,有种让source切换到evalContext相关的依赖链的上下文的意思。

通知去重

Computed/Effect被通知后flags会更新为NOTIFIED,如果在同一个batch里再被通知,就不会重复地加入过batchedEffect的链表中。

批量更新

实现比较简单,batch执行callback、更新value前增加batchDepth,执行完callback以后减少batchDepth,当batchDepth等于1时才逐个执行effect

其他

Computedcallbacklazily evaluate的,而effect则是eager的,这个跟vuecomputedwatchEffect是一样的。

后记

preactblog文,signal的初版是用Set来实现的,但Set的问题是创建会比较expensive,遍历的速度相对比较慢,而且由于依赖的顺序可能会改变,这时候用Set来实现,可能得删除后重新加入,但这样的话文章里说会有新内存分配的可能性,这样会影响性能。而doubly linked-list则很好地解决这些问题。

This article was last updated on days ago, and the information described in the article may have changed.