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


Effect 如果想调度它自身,需要有个排序好的调度表 。Preact 给每个 Effect 实例都添加了专门的 .nextBatchedEffect 属性,让 Effect 实例作为单向调度列表中的节点进行双重作用,这减少了内存抖动,因为反复调度同一个效果不需要额外的内存分配或释放 。
通知订阅和垃圾回收computed signals 实际上并不总是从他们的依赖关系中获取通知的 。只有当有像 effect 这样的东西在监听 signals 本身时,compute signals 才会订阅依赖通知 。这避免了下面的一些情况:
const s = signal(0);{const c = computed(() => s.value)}// c 并不在同一个作用域下如果 c 总是订阅来自 s 的通知,那么 c 无法被垃圾回收,直到 s 也去它这个 scope 上面去 。主要因为 s 会继续挂在一个对 c 的引用上 。
在 Preact Signals 中 , 链表提供了一种比较好的办法去动态订阅和取消订阅依赖通知 。
在那些 computed signal 已经订阅了通知的情况下 , 我们可以利用这个做一些额外的优化 。后面会介绍 computed 懒执行和缓存 。
Computed signals 的懒执行 & 缓存实现懒执行 computed Signals 的最简单方法是每次读取其值时都重新计算 。不过,这不是很高效 。这就是缓存和依赖跟踪需要帮助优化的地方 。
每个普通和 Computed Signals 都有它们自己的版本号 。每次当其值变化时,它们会增加版本号 。当运行一个 compute fn 时 , 它会在 Node 中存储上次看到的依赖项的版本号 。我们原本可以选择在节点中存储先前的依赖值而不是版本号 。然而,由于 computed signals 是懒执行的,这些依赖值可能会永远挂在一些过期或者无限循环执行的 Node 节点上 。因此,我们认为版本编号是一种安全的折中方法 。
我们得出了以下算法 , 用于确定当 computed signals 可以懒执行和复用它的缓存:

  1. 如果自上次运行以来,任何地方的 signal 的值都没有改变,那么退出 & 返回缓存值 。
每次当普通 signal 改变时 , 它也会递增一个全局版本号,这个版本号在所有的普通信号之间共享 。每个计算信号都跟踪他们看到的最后一个全局版本号 。如果全局版本自上次计算以来没有改变,那么可以早点跳过重新计算 。无论如何 , 在这种情况下,都不可能对任何计算值进行任何更改 。
  1. 如果 computed signals 正在监听通知,并且自上次运行以来没有被通知,那么退出 & 返回缓存值 。
当 compute signals 从其依赖项中得到通知时,它标记缓存值已经过时 。如前所述 , compute signals 并不总是得到通知 。但是当他们得到通知时,我们可以利用它 。
  1. 按顺序重新评估依赖项 。检查它们的版本号 。如果没有依赖项改变过它的版本号,即使在重新评估后,也退出 & 返回缓存值 。
这个步骤是我们特别关心保持依赖项按使用顺序排列的原因 。如果一个依赖项发生改变,那么我们不希望重更新 compute list 中后来的依赖项,因为那可能只是不必要的工作 。谁知道,也许那个第一个依赖项的改变导致下次 compute function 运行时丢弃了后面的依赖项 。
  1. 运行 compute function 。如果返回的值与缓存值不同,那么递增计算信号的版本号 。缓存并返回新值 。
这是最后的手段!但如果新值等于缓存的值,那么版本号不会改变 , 而线路下方的依赖项可以利用这一点来优化他们自己的缓存 。
最后两个步骤经常递归到依赖项中 。这就是为什么早期的步骤被设计为尝试短路递归的原因 。
一些思考JSX 渲染Signal 在 Preact JSX 语法进行传值的时候,可以直接传对应的 Signal 对象而不是具体的值,这样在 Signal 对象的值发生变化的时候,可以在组件不经过重新渲染的情况下触发值的变化(本质上是把 Signal 值绑定到 DOM 值上) 。
例如以下组件:
import { render } from 'preact'import { signal } from '@preact/signals'const count = signal(1);// Component 跳过流程是怎么处理// 可能对 state less 的组件跳过 render(function component)funciton Counter() {console.log('render')return (<><p>Count: {count}</p><button notallow={() => count.value ++}>Add Count</button></>)}render(<TodoList />, document.getElement('App'))


推荐阅读