Signal(信号)
细粒度响应式系统的核心原语,封装一个可观察的值。读取时自动注册依赖(依赖收集),写入时自动通知所有订阅者。Solid、Svelte 5、Qwik、Angular Signals 等框架的响应式基础。
核心概念
Signal 是细粒度响应式系统中最基本的状态容器——封装一个值,提供读取(getter)和写入(setter)接口。关键机制:
- 读取时:自动将当前运行的 effect/computation 注册为订阅者(依赖收集)
- 写入时:通知所有订阅者重新执行(触发更新)
这种"读时收集、写时通知"的模式是实现 Fine-grained Reactivity 的基础。
各框架中的 Signal 实现
Solid — createSignal
返回 [getter, setter] 函数对。getter 是一个普通函数,调用 count() 触发依赖收集。所有响应式原语(createMemo、createEffect)共享同一个 getter 接口——调用方无需区分原始 Signal 还是派生值,这是 Solid 响应式组合性极强的根源。
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 3 | Proxy 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 操作,只能自动化缓存检查。
引用本术语的文章
- 响应式系统的四条路:从 Proxy 到 Signal 的追踪粒度全景
- Vercel AI SDK 深度解析:ToolLoopAgent 是 Facade,真正的循环在 generateText 里
- 一个设计决策如何决定一切:五大前端框架源码级横向解剖
- 从 setState 到像素:渲染管线与调度器的完整链路
- 编译器的三重身份:五大前端框架编译管线深度拆解
- 为什么 Attention 能干掉 RNN?
- 【pi-mono 源码解析 1/4】深入 pi-mono Agent Loop:一个 TypeScript AI 代理的上下文管理哲学
- 【pi-mono 源码解析 3/4】幽灵边:pi-mono forkFrom() 揭示的 Lying Sentinel 与 TypeScript 的表达空白
- 从 setTimeout 到 VSync:一篇讲透 Event Loop 的前世今生