信号
什么是信号?
“什么是信号?” 的永久链接信号是用于管理可观察状态的数据结构。
信号可以保存单个值或依赖于其他信号的计算值。信号是可观察的,因此消费者可以在它们发生变化时收到通知。由于它们形成依赖关系图,因此计算信号将在它们的依赖关系发生变化时重新计算并通知消费者。
信号对于建模和管理**共享可观察状态**非常有用 - 许多不同组件可以访问和/或修改的状态。当信号更新时,每个使用和观察该信号或任何依赖于它的信号的组件都将更新。
信号是一个通用概念,在 JavaScript 库和框架中发现许多不同的实现和变体。现在还有一个 TC39 提案,将信号标准化为 JavaScript 的一部分。
信号 API 通常具有三个主要概念
- 状态信号,保存单个值
- 计算信号,包装可能依赖于其他信号的计算
- 观察者或效果,在信号值更改时运行有副作用的代码
以下是如何使用提议的标准 JavaScript 信号 API 的信号示例
//
// Code developers might write to build their signals-based state...
//
// State signals hold values:
const count = new Signal.State(0);
// Computed signals wrap computations that use other signals:
const doubleCount = new Signal.Computed(() => count.get() * 2);
//
// Lower-level code of the sort that will typically be inside frameworks and
// signal-consuming libraries...
//
// Watchers are notified when signals that they watch change:
const watcher = new Signal.subtle.Watcher(async () => {
// Notify callbacks are not allowed to access signals synchronously
await 0;
console.log('doubleCount is', doubleCount);
// Watchers have to be re-enabled after they run:
watcher.watch();
});
watcher.watch(doubleCount);
// Computed signals are lazy, so we need to read it to run the computation and
// potentially notify watchers:
doubleCount.get();
信号库
“信号库” 的永久链接JavaScript 中有许多内置的信号实现。许多都与框架紧密集成,只能从这些框架内部使用,而有些是可从任何其他代码使用的独立库。
虽然特定信号 API 存在一些差异,但它们非常相似。
Preact 的信号库 @preact/signals
是一个独立的库,速度相对快,体积也小,因此我们构建了第一个围绕它的 Lit Labs 信号集成包: @lit-labs/preact-signals
.
JavaScript 信号提案
“JavaScript 信号提案” 的永久链接由于信号 API 之间的强大相似性,框架中使用信号来实现响应性的增加,以及对信号使用系统之间互操作性的渴望,现在正在 TC39 中进行一项提案,以将信号标准化为 https://github.com/tc39/proposal-signals 的一部分。
Lit 提供了 @lit-labs/signals
软件包来与该提案的官方 polyfill 集成。
这个提案对于 Web 组件生态系统来说非常令人兴奋。由于所有采用该标准的库和框架都将生成兼容的信号,因此不同的 Web 组件将不必使用相同的库来以互操作的方式使用和生成信号。
更重要的是,信号有可能成为各种状态管理系统和可观察性库(新的或现有的)的基础。这些库中的每一个,比如 MobX 或 Redux,目前都需要一个特定适配器才能与 Lit 生命周期以符合人体工程学的方式集成。信号标准化可能意味着我们最终只需要一个 Lit 适配器(或者根本不需要适配器,当对信号的支持内置到核心 Lit 库中时)。
信号和 Lit
“信号和 Lit” 的永久链接Lit 目前提供了两个信号集成包: @lit-labs/signals
用于与 TC39 信号提案集成,以及 @lit-labs/preact-signals
用于与 Preact Signals 集成。
由于 TC39 信号提案有望成为 JavaScript 系统收敛到的一个信号 API,我们建议使用它,并将重点放在本文档中的使用情况。
从 npm 安装 @lit-labs/signals
npm i @lit-labs/signals
@lit-labs/signals
提供三个主要导出
SignalWatcher
mixin,应用于使用信号的所有类watch()
模板指令,用于使用精确更新观察单个信号html
模板标记,用于自动将观察指令应用于模板绑定
像这样导入它们
import {SignalWatcher, watch, signal} from '@lit-labs/signals';
@lit-labs/signals
还导出了一些 polyfilled 信号 API 以方便起见,以及一个 withWatch()
模板标记工厂,以便需要自定义模板标记的开发人员可以轻松地添加信号观察功能。
使用 SignalWatcher 自动观察
“使用 SignalWatcher 自动观察” 的永久链接使用信号最简单的方法是在定义自定义元素类时应用 SignalWatcher
mixin。应用 mixin 后,您可以在 Lit 生命周期方法(如 render()
)中读取信号;对这些信号值的任何更改都将自动启动更新。您可以在任何有意义的地方编写信号 - 例如,在事件处理程序中。
在此示例中,SharedCounterComponent
读取和写入共享信号。组件的每个实例都将显示相同的值,并且当值更改时它们都将更新。
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
import {SignalWatcher, signal} from '@lit-labs/signals';
const count = signal(0);
@customElement('shared-counter')
export class SharedCounterComponent extends SignalWatcher(LitElement) {
static styles = css`
:host {
display: block;
}
`;
render() {
return html`
<p>The count is ${count.get()}</p>
<button @click=${this.#onClick}>Increment</button>
`;
}
#onClick() {
count.set(count.get() + 1);
}
}
<!-- Both of these elements will show the same counter value -->
<shared-counter></shared-counter>
<shared-counter></shared-counter>
使用 watch()
进行精确更新
“使用 watch() 进行精确更新” 的永久链接 信号还可以用于实现针对单个绑定(而不是整个组件)的“精确” DOM 更新。为此,我们需要使用 watch()
指令单独观察信号。
出于协调目的,由 watch()
指令触发的更新是分批处理的,并且仍然参与 Lit 响应式更新生命周期。但是,当给定的 Lit 更新纯粹由 watch()
指令触发时,唯一更新的绑定是具有更改信号的绑定;模板中的其余绑定将被跳过。
此示例与上一个示例相同,但仅在 count
信号更改时更新 ${watch(count)}
绑定
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
import {SignalWatcher, watch, signal} from '@lit-labs/signals';
const count = signal(0);
@customElement('shared-counter')
export class SharedCounterComponent extends SignalWatcher(LitElement) {
static styles = css`
:host {
display: block;
}
`;
render() {
return html`
<p>The count is ${watch(count)}</p>
<button @click=${this.#onClick}>Increment</button>
`;
}
#onClick() {
count.set(count.get() + 1);
}
}
请注意,此精确更新避免的工作实际上非常少:唯一跳过的部分是 render()
返回的模板的标识检查和 @click
绑定的值检查,这两者都是廉价的。
事实上,在大多数情况下,watch()
不会 比“普通” Lit 模板渲染带来显着的性能提升。这是因为 Lit 已经只更新了具有更改值的绑定的 DOM。
watch()
的性能节省往往会随着模板逻辑的数量和在更新中可以跳过的绑定数量而增加,因此在具有大量逻辑和绑定的模板中,节省将更加显著。
@lit-labs/signals
还没有包含一个感知信号的 repeat()
指令。在数组的内容发生更改之前,将执行完整渲染。
使用信号 html
模板标记进行自动精确更新
“使用信号 html 模板标记进行自动精确更新” 的永久链接 @lit-labs/signals
还导出了一个特殊的 Lit html
模板标记版本,该版本会自动将 watch()
指令应用于传递给绑定的任何信号值。
这可能很方便,可以避免 watch()
指令的额外字符或没有 watch()
时所需的 signal.get()
调用。
如果您从 @lit-labs/signals
而不是从 lit
导入 html
,您将获得自动观察功能
import {LitElement} from 'lit';
import {SignalWatcher, html, signal} from '@lit-labs/signals';
// SharedCounterComponent ...
render() {
return html`
<p>The count is ${count}</p>
<button @click=${this.#onClick}>Increment</button>
`;
}
信号 html
标记还没有与 lit-analyzer 很好地配合。分析器将在使用信号的绑定上报告类型错误,因为它会看到将 Signal<T>
赋值给 T
。
确保正确安装 polyfill
“确保正确安装 polyfill” 的永久链接@lit-labs/signals
包含 signal-polyfill
软件包作为依赖项,因此您无需显式安装其他任何内容即可开始使用信号。
但是,由于信号依赖于共享的全局数据结构(信号依赖关系图),因此正确安装 polyfill 至关重要:任何页面或应用程序中只能有一份 polyfill 软件包副本。
如果安装了多个 polyfill 副本(无论是由于版本不兼容还是其他 npm 错误),那么就有可能将信号图分区,从而导致某些观察者无法与某些信号配合使用,或者某些信号不会被跟踪为其他信号的依赖项。
为了防止这种情况,请确保只安装了一个 signal-polyfill
,使用 npm ls
命令
npm ls signal-polyfill
如果您看到多个 signal-polyfill
列表,并且在该行旁边没有 deduped
,那么您就有多个 polyfill 副本。
您通常可以通过运行以下命令来解决此问题
npm dedupe
如果这不起作用,您可能需要更新依赖项,直到在整个软件包安装中获得单个兼容版本的 signal-polyfill
。
缺少的功能
“缺少的功能”的永久链接@lit-labs/signals
并非功能完备。一些设想中的功能将使在 Lit 中使用信号更加可行和高效。
- [ ] 一个支持信号的
repeat()
指令。这将使对数组的增量更新更加高效。 - [ ] 一个使用信号进行存储的
@property()
装饰器,用于统一响应式属性和信号。这将使将通用信号实用程序与 Lit 响应式属性一起使用变得更加容易。 - [ ] 一个用于将方法标记为计算信号的
@computed()
装饰器。由于计算信号是记忆的,因此这可以帮助解决昂贵的计算问题。 - [ ] 一个用于将方法标记为效果的
@effect()
装饰器。这可以比使用单独的实用程序更方便地运行效果。
有用资源
“有用资源”的永久链接signal-utils
“signal-utils”的永久链接 signal-utils
npm 包包含许多用于处理 TC39 信号提案的实用程序,包括
- 以信号支持的可观察集合,例如
Array
、Map
、Set
、WeakMap
、WeakSet
和Object
- 用于构建具有信号支持的字段的类的装饰器
- 效果和反应
这些集合和装饰器对于使用信号构建可观察的数据模型很有用,在这种情况下,您通常需要管理比基本类型更复杂的值。
例如,您可以创建一个可观察数组
import {SignalArray} from 'signal-utils/array';
const numbers = new SignalArray([1, 2, 3]);
从数组中读取,例如迭代或读取 .length
将被跟踪为信号访问,并且数组的变异,例如来自 .push()
或 .pop()
,将通知任何观察者。
装饰器
“装饰器”的永久链接装饰器允许您使用可观察字段对类进行建模,就像 LitElement
一样
import {signal} from 'signal-utils';
class GameState {
@signal
accessor playerOneTotal = 0;
@signal
accessor playerTwoTotal = 0;
@signal
accessor over = false;
readonly rounds = new SignalArray();
recordRound(playerOneScore, playerTwoScore) {
this.playerOneTotal += playerOneScore;
this.playerTwoTotal += playerTwoScore;
this.rounds.push([playerOneScore, playerTwoScore]);
}
}
此 GameState
类的实例将由访问它的 SignalWatcher 类跟踪,并在游戏状态更改时更新。
状态和反馈
“状态和反馈”的永久链接此包是 Lit Labs 实验包系列的一部分,目前正在积极开发中。可能存在缺少的功能、实现中的严重错误以及比核心 Lit 库更频繁的重大更改。
此包还依赖于尚未稳定的提案和垫片。随着信号提案的进展,可能会对提议的 API 进行重大更改,这些更改将随后应用于垫片。
我们鼓励谨慎使用,以便我们能够积累使用 Lit 集成层的经验并获得反馈,但请仔细管理依赖关系并进行明智地测试,以最大程度地减少意外重大更改。
请在 @lit-labs/signals 反馈讨论 中留下反馈,并在 遇到任何问题时提交。