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


和 Computed Signals 一样的是,Effect Signals 同样也会对依赖进行追踪 。但 Effect 则不会懒执行 , 与之相反 , 它会在创建的时候立即执行,然后当它追踪的依赖值发生变化的时候,它会随着变化而更新:
import { signal, computed, effect } from '@preact/signals-core';const count = signal(1);const double = computed(() => count.value * 2);const quadrple = computed(() => double.value * 2);effect(() => {// is now 4console.log('quadruple is now', quadruple.value);})count.value = https://www.isolves.com/it/cxkf/yy/Python/2023-12-18/20; // is now 80这里的 effect 执行是由 Preact Signals 内部的通知机制触发的 。当一个普通的 signal 发生变化的时候 , 它会通知它的直接依赖项 , 这些依赖项同样也会去通知它们自己对应的直接依赖项,依此类推 。
在 Preact 的内部实现中,通知路径中的 Computed Signals 会被标记为 OUTDATED 的状态,然后再去做重新执行计算操作 。如果一个依赖变更通知一直传播到一个 effect 上面,那么这个 effect 会被安排到当其自身前面的 effect 函数执行完之后再执行 。
如果你只想调用一次 effect 函数,那么可以把它赋值为一个函数调用,等到这个函数执行完,这个 effect 也会一起结束:
const count = signal(1);const double = computed(() => count.value * 2);const quadruple = computed(() => double.value * 2);const dispose = effect(() => {console.log('quadruple is now', quadruple.value);});// Console: quadruple is now 4dispose();count.value = https://www.isolves.com/it/cxkf/yy/Python/2023-12-18/20;batch(fn)用于将多个值的更新在回调结束时合成为一个 。batch 的处理可以被嵌套 , 并且只有当最外层的处理回调完成后,更新才会刷新:
const name = signal('Dong');const surname = signal('Zoom');// Combine both writes into onebatch(() => {name.value = https://www.isolves.com/it/cxkf/yy/Python/2023-12-18/'Haha';surname.value = 'Nana';})实现方式在开始介绍之前 , 我们结合前面的 API 介绍,来强调一些 Preact Signals 本身的设计性原则:

  • 依赖追踪: 跟踪使用到的 signals(不管是 signals 还是 computed) 。依赖项可能会动态改变
  • 懒执行的 computed: computed 值在被需要的时候运行
  • 缓存: computed 值只在依赖项可能改变的情况下才会重新计算
  • 立即执行的 effect: 当依赖中的某个内容变化时,effect 应该尽快运行 。
关于 Signals 的具体实现方式具体可以参考: https://Github.com/preactjs/signals。
依赖追踪不管什么时候评估实现 compute / effect 这两个函数 , 它们都需要一种在其运行时期捕获他们会读取到的 signal 的方式 。Preact Signals 给 Compute 和 Effect 这两个 Signals 都设置了其自身对应的 context  。
当读取 Signal 的 .value 属性时,它会调用一次 getter ,getter 会将 signal 当成当前 context 依赖项源头给添加进来 。这个 context 也会被这个 signal 添加为其依赖项目标 。
到最后,signal 和 effects 对其自身的依赖关系以及依赖者都会有个最新的试图 。每个 signal 都可以在其 .value 值发生改变的时候通知到它的依赖者 。例如在一个 effect 执行完成之后释放掉了,effect 和 computed signals 都是可以通知他们依赖集去取消订阅这些通知的 。
浅析 Preact Signals 及实现原理

文章插图
图片
同一个 signals 可能在一个 context 里面被读取多次 。在这种情况下,进行依赖项的去重会很方便 。然后我们还需要一种处理 发生变化依赖项集合 的方式: 要么在每次重新触发运行时 时再重建依赖项集合,要么递增地添加/删除依赖项 / 依赖者 。
Preact Signals 在早期版本中使用到了 JS 的 Set 对象去处理这种情况(Set 本身的性能比较不错,能在 O(1) 时间内去添加 / 删除子项,同时能在 O(N) 的时间里面遍历当前集合,对于重复的依赖项,Set 也会自动去重) 。
但创建 Sets 的开销可能相对 Array 要更昂贵(从空间上看),因为 Signals 至少需要创建两个单独的 Sets : 存储依赖项和依赖者 。
浅析 Preact Signals 及实现原理

文章插图
图片
同时 Sets 中也有个属性,它们是按照插入顺序来进行迭代 。这对于 Signals 中处理缓存的情况会很方便,但也有些情况下,Signals 插入的顺序并不是总保持不变的,例如以下情况:


推荐阅读