Signals:前端响应式的新浪潮

5 min

1. 响应式模型的演进

前端开发的本质是“将状态映射为UI”。多年来,我们见证了响应式模型的不断演进:从手动 DOM 操作,到 MVC/MVVM 模式,再到由 React 推广的虚拟DOM (Virtual DOM)。VDOM 通过在组件级别进行状态变更的批量处理和 Diffing,极大地简化了UI开发。

然而,VDOM 并非银弹。其组件级的重新渲染和 Diffing 开销在某些高度动态的场景下会成为性能瓶颈。近年来,一种更古老但经过现代编译器优化的范式——细粒度响应式 (Fine-Grained Reactivity)——重新回到舞台中央,而其核心载体正是 Signals

2. 什么是 Signals?

Signal 是一种响应式原语 (Reactive Primitive),它封装了一个值,并能在该值变化时自动通知其所有依赖项。与 VDOM 框架在组件级别跟踪变化不同,Signal 在数据读取的层面自动建立依赖关系图。

当一个 Signal 的值被更新时,只有直接或间接依赖于该 Signal 的计算(Memo)或副作用(Effect)会被重新执行,从而实现“外科手术式”的精确 DOM 更新,完全绕过了 VDOM Diffing 的需要。

其核心通常由三个原语构成:

  • createSignal(value): 创建一个响应式的 state 单元。
  • createEffect(() => {}): 创建一个自动跟踪依赖并响应变更的副作用。
  • createMemo(() => {}): 创建一个衍生的、带缓存的响应式计算。
// SolidJS 语法示例
const [count, setCount] = createSignal(0);

const doubleCount = createMemo(() => count() * 2);

createEffect(() => {
  console.log(`The double count is: ${doubleCount()}`);
});

setCount(1); // 这将触发 memo 重新计算,并触发 effect 重新执行

一个关键的心智模型是:在基于 Signal 的框架(如 SolidJS)中,组件函数本身只在初始化时运行一次。后续的更新完全由响应式系统驱动,而不是通过组件的重新渲染。

3. 生态系统的广泛采纳

Signal 并非全新概念,其思想可以追溯到 Knockout.js 等早期框架。但借助现代 JavaScript 编译器,它焕发了新的生机,并被各大框架广泛采纳:

  • SolidJS & Qwik: 完全基于 Signal 构建,是该范式的标杆实现。
  • Preact: 通过 @preact/signals 包引入了官方支持,并可以与现有的 Preact/React 项目集成。
  • Vue: 其 Composition API 中的 refcomputed 本质上就是 Signal 的一种实现。
  • Svelte 5: 即将到来的 “Runes” 更新,是 Svelte 团队对响应式模型的一次重构,其核心正是借鉴并实现了 Signal 范式。
  • Angular: 近期版本中也引入了 Signals 作为新的响应式原语。

这种跨框架的趋同,证明了 Signal 作为一种高效、可预测的响应式模型的价值。

4. 优势与权衡

优势:

  1. 卓越的性能: 通过避免 VDOM 和组件级的重渲染,在性能基准测试中通常表现优异。
  2. 可预测性: 状态更新的流向清晰,易于理解和调试。
  3. 内存占用小: 无需维护整个组件树的虚拟表示。

权衡:

  1. 心智模型转变: 对于习惯了 React 渲染周期的开发者来说,需要适应“组件只运行一次”的新模型。
  2. 生态集成: 虽然自身性能强大,但与庞大的 React VDOM 生态(例如,某些UI库)的深度集成可能需要额外的适配工作。

结论

Signals 的复兴标志着前端状态管理范式的一次重要演进。它将开发者的注意力从“组件如何渲染”拉回到了“状态如何流动”这一更本质的问题上。通过与编译器的深度结合,Signal 在提供优秀开发体验的同时,也为 Web 应用的性能边界带来了新的可能性。