Signal(信号)

细粒度响应式系统的核心原语,封装一个可观察的值。读取时自动注册依赖(依赖收集),写入时自动通知所有订阅者。Solid、Svelte 5、Qwik、Angular Signals 等框架的响应式基础。

核心概念

Signal 是细粒度响应式系统中最基本的状态容器——封装一个值,提供读取(getter)和写入(setter)接口。关键机制:

  • 读取时:自动将当前运行的 effect/computation 注册为订阅者(依赖收集)
  • 写入时:通知所有订阅者重新执行(触发更新)

这种"读时收集、写时通知"的模式是实现 Fine-grained Reactivity 的基础。

各框架中的 Signal 实现

Solid — createSignal

返回 [getter, setter] 函数对。getter 是一个普通函数,调用 count() 触发依赖收集。所有响应式原语(createMemocreateEffect)共享同一个 getter 接口——调用方无需区分原始 Signal 还是派生值,这是 Solid 响应式组合性极强的根源。

js
const [count, setCount] = createSignal(0);
createEffect(() => console.log(count())); // count() 触发依赖收集
setCount(1); // 通知 effect 重执行

Svelte 5 — $state Rune

编译器层面的 Signal。开发者写普通赋值语句 let count = $state(0),编译器将其转换为内部 signal 原语 $.source(0),读取时自动插入 $.get(count),写入时插入 $.set(count, newVal)消除了调用 getter 的仪式感,但限制了只能在 .svelte.svelte.ts 文件中使用。

Qwik — useSignal

与 Solid 的 Signal 语义相同,但增加了可序列化约束——Signal 的值必须是 JSON 可序列化类型(不支持 Map/Set/WeakRef/class 实例)。这是 Resumability 架构的要求:服务端渲染时 Signal 状态被序列化到 HTML 属性中,客户端恢复时直接反序列化。

Angular — signal()

Angular 16+ 引入的响应式原语,替代 Zone.js 的全量脏检查。signal() 创建可写 Signal,computed() 创建派生 Signal。Angular 的 Signal 设计受 Solid 影响最大,但集成在 Angular 的依赖注入和模板系统内。

Signal vs 其他状态原语

原语框架追踪方式通知粒度
Signal (createSignal)Solid运行时 getter 调用表达式级
Rune ($state)Svelte 5编译期静态分析表达式级
Ref (ref())Vue 3Proxy getter 拦截属性级→组件级
State (useState)React无自动追踪组件级(需 diff 兜底)

Vue 的 ref 本质上也是一种 Signal,但其追踪粒度止步于属性级——通知到组件的渲染 effect,组件内部仍走 VNode diff。严格意义上 ref 是"粗粒度 Signal"。

与编译器的关系

Signal 的存在是编译器能生成精确 DOM 更新代码的前提条件。编译器看到 {count} 模板绑定时,能否生成 set_text(text, count的新值) 这样的直接 DOM 操作,取决于 count 是否是一个 Signal——只有 Signal 提供了"值变化时精确通知"的能力,编译器才有可能将通知终点绑到具体 DOM 节点。React 的 useState 不提供这个能力,所以 React 的编译器(React Compiler)无法生成直接 DOM 操作,只能自动化缓存检查。

引用本术语的文章