Signals 来了:前端响应式编程正在发生的范式转移
- 作者:Bougie
- 创建于:2026-06-12

凌晨两点,你的屏幕上是一堆 useState、useEffect、useMemo、useCallback,还有那些永远记不清的依赖数组。你刚修好一个 bug,又引出了两个新 bug——一个下拉框的值变了,但某个远处的图表没更新,因为 useEffect 的依赖数组里漏写了那个状态。
或者你是 Vue 开发者,你习惯了 .value 的仪式感,每次取数都要写 count.value,每次改值都要写 count.value++。当你试图在模板里直接用 count 时,Vue 会礼貌地报错:'count' is not a reactive value。你开始怀疑人生:为什么我不能用直觉编程?
更让人沮丧的是性能问题。你有一个包含 1000 个项的长列表,每次"选中一个"这个简单操作,整个列表都要重新渲染。原因很简单:React 的 useState 只知道"状态变了",不知道"哪个地方用了这个状态"。它只能把整个组件树都跑一遍,做虚拟 DOM diff。
这些问题,你我都遇到过。它们有一个共同的名字:命令式状态管理与声明式 UI 之间的鸿沟。
而今天我要讲的 Signals,正是这个问题的一种根本性解法。它不是新东西——早在 2012 年 Knockout.js 就玩过这一套——但它正在以全新的姿态杀回来,而且这次,是所有主流框架都在拥抱它。
让我们开始。
# Signals 究竟是什么:一个比喻
想象你是公司前台。你不需要知道谁打了电话、你只需要在电话响的时候接起来。当有人来找你,说"如果有人打电话来,通知我一声",你就在心里记一笔,然后等电话响的时候去通知那个人。
这就是 Signals 的核心理念:数据拥有"通知"的能力,而不需要数据的消费者主动去"轮询"或"订阅"。
# 用代码说话
// Signals 的最小形态
import { signal, computed, effect } from '@solidjs/signals';
// 创建一个信号 - 就像一个会喊"我变了"的值容器
const count = signal(0);
// 派生值 - 自动追踪依赖,依赖不变就不重新计算
const doubled = computed(() => count.value * 2);
// 副作用 - 自动追踪它用到的所有信号
effect(() => {
console.log(`计数是 ${count.value},双倍是 ${doubled.value}`);
});
// 改变值
count.value = 1; // 输出: "计数是 1,双倍是 2"
count.value = 5; // 输出: "计数是 5,双倍是 10"
你注意到了吗?count.value 的赋值没有调用任何订阅函数,没有 useEffect,没有 useMemo,只有简单的赋值。但 doubled 自动更新了,effect 自动重新执行了。
这就是 Signals 的魔力:细粒度的自动依赖追踪。
# 对比 React 的 useState
// React useState - 你需要"显式声明"你的依赖
function Counter() {
const [count, setCount] = useState(0);
const [doubled, setDoubled] = useState(0);
// 你必须手动同步 doubled 和 count 的关系
useEffect(() => {
setDoubled(count * 2);
}, [count]); // 漏写这个依赖?恭喜你,bug 来了
// 而且这个 effect 会在每次渲染后运行
// 如果里面有异步操作?混乱开始
return <button onClick={() => setCount(count + 1)}>{doubled}</button>;
}
# 对比 Vue 的 ref
// Vue 3 ref - 你需要 .value 的仪式感
import { ref, computed, watch } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
// 每次取值都要写 .value
console.log(count.value);
count.value++;
// 如果想在 count 变化时做点什么
watch(count, (newVal) => {
console.log(`计数变了: ${newVal}`);
});
Vue 的 ref 其实是 Signals 的前身,但 .value 这个语法糖既是一种保护(让你区分响应式和非响应式),也是一种负担(每次写都很烦)。

# Signals 的本质
你可以把 Signals 想象成一种智能的、会自动广播的值容器:
- 信号 (Signal):一个可变值,当它被修改时,会通知所有"订阅"了它的地方
- 计算值 (Computed):一个只读的派生值,它自动订阅自己的依赖
- 副作用 (Effect):一段会自动订阅它读取的所有信号,并在信号变化时重新运行的代码
这三个概念构成了 Signals 的全部。与 React 的"状态驱动渲染"不同,Signals 是"状态驱动更新"——更新精确到使用这个值的地方,而不需要重新渲染整个组件。
# 三大主流框架的拥抱现状
# SolidJS:原生实现,性能怪兽
SolidJS 是第一个把 Signals 作为核心竞争力的框架。从第一天起,它就没有虚拟 DOM,没有 useState,只有 Signals。
// SolidJS - Signals 原生使用
import { createSignal, createMemo, createEffect } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
// computed - 响应式派生
const doubled = createMemo(() => count() * 2);
// effect - 响应式副作用
createEffect(() => {
console.log(`双倍值: ${doubled()}`);
});
return (
<div>
<p>计数: {count()}</p>
<p>双倍: {doubled()}</p>
<button onClick={() => setCount(c => c + 1)}>加一</button>
</div>
);
}
注意这里 count() 是函数调用形式,而不是 count.value。SolidJS 的信号是函数,这种设计让它可以在编译时静态分析访问模式,从而生成精确的更新代码。
SolidJS 的性能数据:
- 在 js-framework-benchmark 中,SolidJS 的性能与 vanilla JS 几乎一样快,比 React 快 10-50 倍
- 没有虚拟 DOM diff,直接精确更新 DOM 节点
- 编译时优化,信号访问可以被静态分析
# Vue 3.4+:实质就是 Signals
Vue 3 在 2019 年引入了 Composition API,其中的 ref 和 reactive 本质上就是 Signals 的变体。Vue 3.4 更是做了重要改进:
// Vue 3.4 - ref 的 .value 可以不写了(仅限 setup)
import { ref, computed, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
// computed - 响应式派生
const doubled = computed(() => count.value * 2);
// effect - watchEffect 自动追踪
watchEffect(() => {
console.log(`双倍值: ${doubled.value}`);
});
const increment = () => count.value++;
return { count, doubled, increment };
}
}
Vue 的 ref 在模板中会自动解包 .value,所以在模板里写 是可以的。这是一种"魔法",也是 Vue 的特色。
2024 年,Vue 团队正式宣布 Vue 的响应式系统与 Signals 的概念高度一致,并且暗示未来版本会进一步融合 Signals 特性。
# Angular 17+:Signals 作为官方 API
Angular 在 2023 年 11 月的 Angular 17 中引入了 Signals 作为官方状态管理方案:
// Angular 17 - Signals API
import { signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>计数: {{ count() }}</p>
<p>双倍: {{ doubled() }}</p>
<button (click)="increment()">加一</button>
`
})
export class CounterComponent {
// signal - 响应式值容器
count = signal(0);
// computed - 派生值
doubled = computed(() => this.count() * 2);
// effect - 副作用
constructor() {
effect(() => {
console.log(`双倍值: ${this.doubled()}`);
});
}
increment() {
this.count.update(c => c + 1);
}
}
Angular 的 Signals 集成了 Zone.js 的变化检测机制,但 Signals 本身是独立工作的——你可以选择只用 Signals 而不用 Zone.js,性能会更好。
# React:TC39 标准化提案进行时
2023 年 9 月,一份关于将 Signals 引入 JavaScript 的提案提交到了 TC39(JavaScript 的标准委员会)。提案的作者来自 Chrome、Meta、GitHub 等公司。
// TC39 Signals 提案(草案阶段)
// 注意:这是未来可能的 JavaScript 标准 API,不是现在的 React
const count = Signal.global({ value: 0 });
const doubled = Signal.computed(() => count.get() * 2);
Effect.root(() => {
Effect.watch(() => {
console.log(`双倍值: ${doubled.get()}`);
});
count.set(5); // 输出: "双倍值: 10"
});
如果这个提案通过,Signals 将成为 JavaScript 语言的一部分,所有框架都可以使用。这将是前端历史上最大的范式转移之一。

# 实战代码对比:同一个例子,三种写法
让我们用一个具体的例子来对比:实现一个计数器 + 派生值 + 异步副作用的组件。
# 需求描述
- 一个计数器,点击按钮 +1
- 显示计数器的双倍值(派生)
- 显示计数器的平方(派生)
- 当计数超过 100 时,发送 API 请求(异步副作用)
- 显示"请求中"和"请求完成"的状态
# React 版本
// React 实现
import { useState, useEffect, useCallback, useRef } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [doubled, setDoubled] = useState(0);
const [squared, setSquared] = useState(0);
const [status, setStatus] = useState('idle');
const [apiResult, setApiResult] = useState(null);
const requestIdRef = useRef(0);
// 同步派生值 - 每次 count 变化都要手动更新
useEffect(() => {
setDoubled(count * 2);
setSquared(count * count);
}, [count]);
// 异步副作用 - 依赖追踪的噩梦
useEffect(() => {
if (count > 100) {
setStatus('loading');
const requestId = ++requestIdRef.current;
fetch(`/api/heavy?count=${count}`)
.then(res => res.json())
.then(data => {
// 防止竞态
if (requestId === requestIdRef.current) {
setApiResult(data);
setStatus('done');
}
})
.catch(() => {
if (requestId === requestIdRef.current) {
setStatus('error');
}
});
}
}, [count]); // 依赖数组,count 变化就重新执行
return (
<div>
<p>计数: {count}</p>
<p>双倍: {doubled}</p>
<p>平方: {squared}</p>
<p>API 状态: {status}</p>
{apiResult && <p>API 结果: {JSON.stringify(apiResult)}</p>}
<button onClick={() => setCount(c => c + 1)}>加一</button>
</div>
);
}
# Vue 3 版本
// Vue 3 实现
import { ref, computed, watch } from 'vue';
export default {
setup() {
const count = ref(0);
const status = ref('idle');
const apiResult = ref(null);
// computed - 自动追踪依赖
const doubled = computed(() => count.value * 2);
const squared = computed(() => count.value * count.value);
// watch - 异步副作用
watch(count, async (newCount) => {
if (newCount > 100) {
status.value = 'loading';
try {
const res = await fetch(`/api/heavy?count=${newCount}`);
const data = await res.json();
apiResult.value = data;
status.value = 'done';
} catch {
status.value = 'error';
}
}
}, { immediate: false });
const increment = () => count.value++;
return { count, doubled, squared, status, apiResult, increment };
}
}
# SolidJS / Signals 版本
// SolidJS Signals 实现
import { createSignal, createMemo, createEffect } from 'solid-js';
import { createResource } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
// computed - 简洁的派生值
const doubled = createMemo(() => count() * 2);
const squared = createMemo(() => count() * count());
// createResource - 内置的异步数据管理
const [apiData] = createResource(
count, // 自动追踪这个信号
async (currentCount) => {
if (currentCount > 100) {
const res = await fetch(`/api/heavy?count=${currentCount}`);
return res.json();
}
return null;
}
);
return (
<div>
<p>计数: {count()}</p>
<p>双倍: {doubled()}</p>
<p>平方: {squared()}</p>
<p>API 状态: {apiData.loading ? 'loading' : apiData() ? 'done' : 'idle'}</p>
<Show when={apiData()}>
<p>API 结果: {JSON.stringify(apiData())}</p>
</Show>
<button onClick={() => setCount(c => c + 1)}>加一</button>
</div>
);
}
# 对比总结
| 维度 | React | Vue 3 | SolidJS/Signals |
|---|---|---|---|
| 派生值 | 需要 useEffect + useState | computed | createMemo |
| 异步副作用 | useEffect + 竞态处理 | watch + async | createResource 内置处理 |
| 依赖追踪 | 手动声明依赖数组 | 自动追踪 | 自动追踪 |
| 样板代码 | 多 | 中等 | 少 |
| 运行时开销 | 虚拟 DOM + diff | Proxy + 模板编译 | 编译时优化,零运行时 |

# 性能与心智:Signals 的得与失
# 性能优势
1. 精确更新,告别过度渲染
传统的虚拟 DOM diff 需要比较整个组件树,但 Signals 可以精确到每一个使用信号的地方。
场景:1000 个列表项,其中一项的计数增加
React:
- 触发重新渲染
- 虚拟 DOM diff 整个组件树
- 假设 diff 结果是"只变化了一个元素"
- 但 React 需要重新渲染整个列表的父组件
Signals:
- 触发 count signal 更新
- 只重新计算/渲染使用了 count 的那个 DOM 节点
- 其他 999 个节点完全不动
在 js-framework-benchmark 的"replace all rows"测试中,SolidJS 的性能是 React 的 50 倍。不是 50%,是 50 倍。
2. 动画与实时数据
Signals 的细粒度更新特别适合:
- 实时数据可视化(股票行情、游戏状态)
- 动画(每一帧只更新需要变化的元素)
- 大量表单(只验证真正变化的字段)
3. 内存与初始化
Signals 不需要创建组件实例来管理状态。状态是独立的值容器,不与特定的组件生命周期绑定。
# 客观看待的不足
1. 学习曲线
Signals 引入了新的心智模型:
- "订阅是自动的":这与 React 的"显式依赖"完全不同
- 时序问题:当 effect A 读取 signal B,而 effect B 读取 signal A 时,会发生什么?
- 计算循环:computed 值是否可以依赖自己?
2. 调试工具不成熟
React 有强大的 React DevTools,可以查看组件树、状态历史、性能分析。Signals 的调试主要靠:
console.log打印值- 各框架自己的开发工具(Solid DevTools、Vue DevTools 的响应式面板)
- 没有统一的标准
3. 生态系统尚浅
- React 生态有 8 年的积累,所有第三方库都是基于 hooks/class 的
- Signals 是新范式,很多库还没有适配
- TypeScript 类型推断有时不理想
4. SSR 挑战
服务端渲染时,Signals 的水合(hydration)过程比 React 更复杂,因为需要重建客户端的订阅关系。
# 我的判断与怀疑:从业者视角
# 这次不是炒作
我见过太多"银弹"了。Angular 1 的"下一次Web开发方式",React 的"革命性UI库",Vue 的"渐进式框架",Svelte 的"删掉 runtime",每个都声称要改变一切。
但 Signals 不一样。
因为这次,所有框架都在跟进。React 在推动 TC39 标准化,Vue 已经在用,Angular 官方采纳,SolidJS 已经证明可行。这不是某一家公司的营销,而是整个行业对同一个解法的共识。
这让我想起 2015 年 ES6 的标准化——Promise、async/await、class 语法,这些"新特性"最后都成为了标准,改变了所有代码。Signals 可能也是这样的机会。
# 但也请保持怀疑
问题 1:真的会标准化吗?
TC39 的提案需要经过多轮讨论和实现,周期可能是 3-5 年。而且即使标准化了,框架的实现细节也会不同。Vue 的 signals 和 Solid 的 signals 可能不是一回事。
问题 2:迁移成本
全球有几千万 React 开发者,他们的代码都是基于 hooks 的。你告诉他们"忘掉 useState 吧"?这不是一个 SignalState 能解决的事。
问题 3:虚拟 DOM 真的那么糟吗?
虚拟 DOM 是 React 的核心,也是它最大的优势之一:简单、抽象、与平台无关。Signals 精确但也耦合——它需要知道你具体在哪个 DOM 节点渲染这个值。
问题 4:调试问题怎么解决?
这是我认为最大的风险。如果调试工具不成熟,开发者就会觉得 Signals 是个"黑盒"。而"黑盒"在前端开发中是不可接受的——我们需要能够追踪 bug、理解行为。
# 前端框架之争的终点
我有一个预测:前端框架的终点不是某个具体的框架,而是"声明式 + 响应式"成为默认范式。
未来的前端,可能是这样的:
- JSX/模板 负责声明 UI 结构
- Signals/状态 负责声明数据和它的派生关系
- 编译器负责优化,生成高性能的更新代码
- 框架的差异主要在于语法糖和生态,而核心范式统一
就像今天没有人在讨论"应该用面向对象还是面向过程"一样,10 年后可能也没有人在讨论"应该用 React 还是 Vue",因为它们都是"声明式 UI + 响应式状态"的变体。

# 行动建议:普通开发者现在应该做什么?
# 如果你是 React 开发者
现在不需要立刻切换框架,但需要:
- 学习 Signals 的思维方式,理解"自动订阅"的含义
- 关注 React 社区对 Signals 的探索(比如
preact/signals,@lit-labs/task) - 等待 React 官方的 Signals 支持(如果他们真的做的话)
- 保持对 useState/useEffect 的熟练度——它还会存在很久
# 如果你是 Vue 开发者
你已经在一个 Signals-like 的世界里了:
- 深入理解
ref/reactive/computed的原理 - 尝试在不用 Vue 的场景下使用 Signals(比如用
solid-js写独立组件) - Vue 3.4+ 的 Composition API 已经足够强大,先用好它
# 如果你想学 Signals
从 SolidJS 开始:
npm create solid
SolidJS 是最纯粹的 Signals 实现,没有历史包袱,文档优秀,性能极致。你可以用它写一个 Todo App,感受 Signals 的工作方式。
学习资源:
- SolidJS 官方文档:solidjs.com
- 《Deep Dive into Signals》(Ryan Carniato 的博客,SolidJS 作者)
- TC39 Signals 提案:github.com/tc39/proposal-signals
# 如果你是框架作者或架构师
现在做技术选型决策时:
- 如果你在做性能敏感的应用(游戏、数据可视化、实时系统),考虑 SolidJS
- 如果你在做企业级应用,Vue 3 或 React 仍然可靠
- 如果你在评估未来技术趋势,Signals 是值得押注的方向
# 尾声
凌晨两点,你终于修好了那个 bug。屏幕上不再有混乱的依赖数组,不再有漏写的 useEffect,不再有"为什么这里没更新"的困惑。
你写下了:
const count = signal(0);
const doubled = computed(() => count() * 2);
effect(() => {
console.log(`双倍值: ${doubled()}`);
});
count.value = 100;
它工作了。就像你直觉中应该工作的那样。
前端开发的范式正在转移。你可以选择观望,也可以选择站在潮头。但无论如何,Signals 教会我们的最重要的事情是:
好的抽象应该让复杂变简单,而不是把简单变复杂。