前言
最新的一期Javascript Weekly记载Vue 3.5发布了,其中第一点改动提到了重构它reactity的实现。而这个实现的灵感来自于preact的signal,使用了doubly linked-list和version number,而它的核心源码只有一个文件,算是比较易读的。
本文
数据结构
1 | type Node = { |
source表示依赖项,target表示订阅者,sources通常存的是依赖链,是正序的,而targets存的是订阅链,跟倒序的。targets倒序可能是因为batched effect是先进后出的,这样子从末尾开始notify可以保证effect按正向执行。
依赖追踪
初始时,首先会在Computed/Effect执行callback前设自己为全局的evalContext,在callback执行时如果get signal的value,则会创建一个node, 其source设为这个signal,target设为这个Computed/Effect,再把这个node加入到Computed/Effect的依赖链中以及signal的订阅链中。
随后,当某些source发生变化时,会执行这些订阅者的callback。而此时,订阅者会执行prepareSources这个步骤,目的是通过将依赖链的所有node的version置为-1,标记上次的依赖项,当callback执行完,在cleanupSources这一步中,把version依然是-1的node剔除出依赖链;prepareSources除上述的作用以外,还会把node设到source._node里,_node其实是跟当前evalContext关联的,有种让source切换到evalContext相关的依赖链的上下文的意思。
通知去重
Computed/Effect被通知后flags会更新为NOTIFIED,如果在同一个batch里再被通知,就不会重复地加入过batchedEffect的链表中。
批量更新
实现比较简单,batch执行callback、更新value前增加batchDepth,执行完callback以后减少batchDepth,当batchDepth等于1时才逐个执行effect
其他
Computed的callback是lazily evaluate的,而effect则是eager的,这个跟vue的computed和watchEffect是一样的。
后记
看preact的blog文,signal的初版是用Set来实现的,但Set的问题是创建会比较expensive,遍历的速度相对比较慢,而且由于依赖的顺序可能会改变,这时候用Set来实现,可能得删除后重新加入,但这样的话文章里说会有新内存分配的可能性,这样会影响性能。而doubly linked-list则很好地解决这些问题。