浅析 Preact Signals 及实现原理

介绍Preact Signals 是 Preact 团队在22年9月引入的一个特性 。我们可以将它理解为一种细粒度响应式数据管理的方式,这个在很多前端框架中都会有类似的概念,例如 SolidJS、Vue3 的 Reactivity、Svelte 等等 。
Preact Signals 在命名上参考了 SolidJS 的 Signals 的概念,不过两个框架的实现方式和行为都有一些区别 。在 Preact Signals 中,一个 signal 本质上是个拥有 .value 属性的对象,你可以在一个 React 组件中按照如下方式使用:
import { signal } from '@preact/signals';const count = signal(0);function Counter() {const value = https://www.isolves.com/it/cxkf/yy/Python/2023-12-18/count.value;return (

Count: {value}

)}通过这个例子,我们可以看到 Signal 不同于 React Hooks 的地方: 它是可以直接在组件外部调用的 。
同时这里我们也可以看到 , 在组件中声明了一个叫 count 的 signal 对象,但组件在消费对应的 signal 值的时候,只用访问对应 signal 对象的 .value 值即可 。
在开始具体的介绍之前,笔者先从 Preact 官方文档中贴几个关于 Signal API 的介绍,让读者对 Preact Signals 这套数据管理方式有个基本的了解 。
API以下为 Preact Signals 提供的一些 Common API:
signal(initialValue)这个 API 表示的就是个最普通的 Signals 对象 , 它算是 Preact Signals 整个响应式系统最基础的地方 。
当然,在不同的响应式库中,这个最基础的原语对象也会有不同的名称,例如 Mobx、RxJS 的 Observers,Vue 的 Refs 。而 Preact 这里参考了和 SolidJS 一样的术语 signal 。
Signal 可以表示包装在响应式里层的任意 JS 值类型,你可以创建一个带有初始值的 signal,然后可以随意读和更新它:
import { signal } from '@preact/signals-core';const s = signal(0);console.log(s.value); // Console: 0s.value = https://www.isolves.com/it/cxkf/yy/Python/2023-12-18/1;console.log(s.value); // Console: 1computed(fn)Computed Signals 通过 computed(fn) 函数从其它 signals 中派生出新的 signals 对象:
import { signal, computed } from '@preact/signals-core';const s1 = signal('hello');const s2 = signal('world');const c = computed(() => {return s1.value + " " + s2.value})不过需要注意的是,computed 这个函数在这里并不会立即执行 , 因为按照 Preact 的设计原则,computed signals 被规定为懒执行的(这个后面会介绍),它只有在本身值被读取的时候才会触发执行,同时它本身也是只可读的:
console.log(c.value) // hello world同时 computed signals 的值是会被缓存的 。一般而言 , computed(fn) 运行开销会比较大,Preact 只会在真正需要的时候去重新更新它 。一个正在执行的 computed(fn) 会追踪它运行期间读取到的那些 signals 值,如果这些值都没变化,那么是会跳过重新计算的步骤的 。
因此在上面的示例中 , 只要 s1.value 和 s2.value 的值不变化,那么 c.value 的值永远不会重新计算 。
【浅析 Preact Signals 及实现原理】同样,一个 computed signal 也可以被其它的 computed signal 消费:
const count = signal(1);const double = computed(() => count.value * 2);const quadruple = computed(() => double.value * 2);console.log(quadruple.value); // Console: 4count.value = https://www.isolves.com/it/cxkf/yy/Python/2023-12-18/20;console.log(quadruple.value); // Console: 80同时 computed 依赖的 signals 也并不需要是静态的 , 它只会对最新的依赖变更发生重新执行:
const choice = signal(true);const funk = signal("Uptown");const purple = signal("Haze"); const c = computed(() => {if (choice.value) {console.log(funk.value, "Funk");} else {console.log("Purple", purple.value);}});c.value; // Console: Uptown Funkpurple.value = https://www.isolves.com/it/cxkf/yy/Python/2023-12-18/"RAIn"; // purple is not a dependency, so c.value; // effect doesn't runchoice.value = false; c.value; // Console: Purple Rain funk.value = "Da"; // funk not a dependency anymore, so c.value; // effect doesn't run我们可以通过这个 Demo 看到,c 这个 computed signal 只会在它最新依赖的 signal 对象值发生变化的时候去触发重新执行 。
effect(fn)上一节中介绍的 Computed Signals 一般都是一些不带副作用的纯函数(所以它们可以在初次懒执行) 。这节要介绍的 Effect Signals 则是用来处理一些响应式中的副作用使用 。


推荐阅读