浅析 Preact Signals 及实现原理( 三 )


const s1 = signal(0)const s2 = signal(0)const s3 = signal(0)const c = computed(() => {if (s1.value) {s2.value;s3.value} else {s3.values2.value}})可以看到,这这次代码中,依赖项的顺序取决于 s1 这个 signal,顺序要么是 s1、s2、s3,要么是 s1、s3、s2 。按照这种情况,就必须采取一些其他的步骤来保证 Sets 中的内容顺序是正常的: 删除然后再添加项目,清空函数运行前的集合,或者为每次运行创建一个新的集合 。每种方法都有可能导致内存抖动 。而所有这些只是为了处理理论上可能,但可能很少出现的,依赖关系顺序改变的情况 。
而 Preact Signals 则采用了一种类似双向链表的数据结构去存储解决了这个问题 。
链表链表是一种比较原始的存储结构,但对于实现 Preact Signals 的一些特点来说,它具备一些非常好的属性 , 例如在双向链表节点中,以下操作会非常节省:

  • 在 O(1) 时间内,将一个 signals 值插到链表的某一端
  • 在 O(1) 时间内 , 删除链表任何位置的一个节点(假设存在对应指针的情况下)
  • 在 O(n) 时间内,遍历链表中的节点
以上这些操作 , 都可以用于管理 Signals 中的依赖 / 依赖列表 。
Preact 会首先给每个依赖关系都创建一个 source Node  。而对应 Node 的 source 属性会指向目前正在被依赖的 Signal 。同时每个 Node 都有 nextSource 和 prevSource 属性,分别指向依赖列表中的下一个和前一个 source Nodes  。Effect 和 Computed Signals 获得一个指向链表第一个 Node 的 sources 属性,然后我们可以去遍历这里面的一些依赖关系,或者去插入 / 删除新的依赖关系 。
浅析 Preact Signals 及实现原理

文章插图
图片
然后处理完上面的依赖项步骤后 , 我们再反过来去做同样的事情: 给每个依赖者创建一个 Target Node  。Node 的 target 属性则会指向它们依赖的 Effect 或 Computed Signals 。nextTarget 和 prevTarget 构建一个双项链表 。普通和 computed Signals Node 节点中会有个targets 属性用于指向他们依赖列表中的第一个 Target Node:
浅析 Preact Signals 及实现原理

文章插图
图片
但一般依赖项和依赖者都是成对出现的 。对于每个 source Node 都会有一个对应的 target Node  。本质上我们可以将 source Nodes 和 target Nodes 统一合并为 Nodes  。这样每个 Node 本质上会有四条链节,依赖者可以作为它依赖列表的一部分使用 , 如下图所示:
浅析 Preact Signals 及实现原理

文章插图
图片
在每个 computed / effect 函数执行之前 , Preact 会迭代以前的依赖关系,并设置每个 Node 为 unused 的标志位 。同时还会临时把 Node 存储到它的 .source.node 属性中用于以后使用 。
在函数执行期间,每次读取依赖项时,我们可以使用节点以前记录的值(上次的值)来发现该依赖项是否在这次或者上次运行时已经被记录下来,如果记录下来了,我们就可以回收它之前的 Node(具体方式就是将这个节点的位置重新排序) 。如果是没见过的依赖项 , 我们会创建一个新的 Node 节点,然后将剩下的节点按照使用的时期进行逆序排序 。
函数运行结束后,Preact Signals 会遍历依赖列表 , 将打上了 unused 标志的 Nodes 节点给删除掉 。然后整理一下剩余的链表节点 。
这种链表结构可以让每次只用给每个依赖项 - 依赖者的关系对分配一个 Node,然后只要依赖关系是存在的,这个节点是可以一直用的(不过需要更新下节点的顺序而已) 。如果项目的 Signals 依赖树是稳定的,内存也会在构建完成后一直保持稳定 。
立即执行的 effect有了上面依赖追踪的处理 , 通过变更通知实现的立即执行的 effect 会很容易 。Signals 通知其依赖者们,自己的值发生了变化 。如果依赖者本身是个有依赖者的 computed signals,那么它会继续往前传递通知 。依此类推,接到通知的 effect 会自己安排自己运行 。
如果通知的接收端,已经被提前通知了 , 但还没机会执行,那它就不会向前传递通知了 。这会减轻当前依赖树扩散出去或者进来时形成的通知踩踏 。如果 signals 本身的值实际上没发生变化,例如 s.value = https://www.isolves.com/it/cxkf/yy/Python/2023-12-18/s.value 。普通的 signal 也不会去通知它的依赖者 。


推荐阅读