理解 Signal:下一代前端响应式核心机制
个人对于sign的理解
一、为什么会有 Signal?
在前端响应式编程的发展历史中,我们经历了多个阶段:
- MVC 双向绑定(AngularJS)
- 虚拟 DOM + Diff(React、Vue2)
- Proxy 响应式(Vue3)
- Hooks 状态管理(React 16+)
- Vue3.6+ 中的 alien-signal
虽然这些方案都解决了"数据驱动视图"的核心问题,但它们普遍存在渲染粒度粗 的问题:
一个状态变化,可能会导致整个组件或子树重新渲染,即使只有一个小部分依赖它。
Signal 的出现,就是为了让状态更新的粒度更细、更直接------只更新依赖它的那部分视图。
二、什么是 Signal?
一句话定义:
Signal 是一个可订阅的值容器,当值发生变化时,会通知所有依赖它的计算或副作用函数更新。
在现代框架中,Signal 通常分为三类:
- Writable Signal
可写的信号,存储可变状态 - Computed Signal
由其他信号派生的只读信号 - Effect
副作用函数,当依赖的信号变化时执行
简单例子:
js
import { signal, computed, effect } from 'some-signal-lib';
const count = signal(0);
const doubleCount = computed(() => count() * 2);
effect(() => {
console.log("Count:", count(), "Double:", doubleCount());
});
count.set(1); // 自动触发 effect
三、Signal 与 Vue3 响应式的底层对比
特性 | Vue3 reactive/ref | Signal |
---|---|---|
实现方式 | Proxy / getter | getter + 订阅列表(Set) |
依赖追踪方式 | track / trigger(WeakMap) | 显式注册到 subscribers |
更新粒度 | 属性级别 | 值级别(更细) |
调度机制 | 全局 effect 栈 + Map 存储 | Signal 对象内部直接存储订阅者 |
应用场景 | 通用响应式系统 | 高性能组件更新 / 精细化状态管理 |
Vue3 的响应式是全局化的依赖管理,Signal 则更像是局部响应式容器,订阅和触发都局限在这个对象本身。
四、Signal 的实现原理
Signal 内部一般维护两个东西:
- value:当前信号值
- subscribers:依赖它的副作用函数集合(Set 去重)
4.1 依赖收集
当读取信号值时,如果存在当前活跃的副作用函数 ,就将它加入 subscribers
。
4.2 更新通知
当信号值变化时,循环执行 subscribers
中的副作用函数。
flowchart TD
A[effect 执行] --> B[读取 signal 值]
B --> C[注册 effect 到 subscribers]
C --> D[更新 signal 值]
D --> E[遍历 subscribers]
E --> F[执行 effect]
五、极简版 Signal 实现
下面的代码实现了最基本的 signal
和 effect
,可直接在浏览器运行:
js
let activeEffect = null;
function signal(initial) {
const subscribers = new Set();
let value = initial;
const read = () => {
if (activeEffect) subscribers.add(activeEffect);
return value;
};
const write = (newValue) => {
value = newValue;
subscribers.forEach(fn => fn());
};
return [read, write];
}
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
// 测试
const [count, setCount] = signal(0);
effect(() => {
console.log("count changed:", count());
});
setCount(1);
setCount(2);
六、Computed Signal 的实现
在这个版本里,我们添加派生信号(computed
):
js
function computed(getter) {
const [read, write] = signal();
effect(() => write(getter()));
return read;
}
// 测试
const [count, setCount] = signal(0);
const doubleCount = computed(() => count() * 2);
effect(() => {
console.log("Double:", doubleCount());
});
setCount(3); // Double: 6
七、Signal 与 Vue3 响应式的对比
Vue3 的响应式和 Signal 的相似点:
- 都有 副作用函数(effect)
- 都有 依赖收集(track) 和 触发更新(trigger)
但它们的依赖存储结构和触发路径完全不同。
7.1 数据结构差异
Vue3 响应式依赖存储
js
WeakMap (targetMap)
-> Map (depsMap)
-> Set (dep)
-> effect 函数
-
targetMap 记录不同的对象
-
depsMap 记录对象的不同属性
-
dep 存储依赖该属性的所有 effect
Signal 依赖存储
js
Signal 对象
-> subscribers (Set)
-> effect 函数
-
依赖直接存在 Signal 实例上
-
没有全局 WeakMap,也不区分属性级别
7.2 执行路径对比
Vue3 track/trigger 流程
sequenceDiagram
participant User as 用户访问/修改属性
participant Proxy as Proxy 拦截
participant Track as track()
participant TargetMap as targetMap
participant Trigger as trigger()
participant Effect as effect()
User->>Proxy: get/set 属性
Proxy->>Track: 调用 track/trigger
Track->>TargetMap: 查找 target
TargetMap->>TargetMap: 获取/创建 depsMap
TargetMap->>TargetMap: 获取/创建 dep
Track->>Effect: 添加 activeEffect
Trigger->>TargetMap: 获取 depsMap
TargetMap->>Trigger: 获取 dep
Trigger->>Effect: 执行所有依赖
Signal 订阅/触发流程
sequenceDiagram
participant User as 用户读取/设置值
participant Signal as Signal 对象
participant Subs as subscribers(Set)
participant Effect as effect()
User->>Signal: 调用 read()
Signal->>Subs: 添加 activeEffect
User->>Signal: 调用 write(newValue)
Signal->>Subs: 遍历 subscribers
Subs->>Effect: 执行所有依赖
7.3 精简源码对比
Vue3 核心
js
function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, depsMap = new Map());
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, dep = new Set());
dep.add(activeEffect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
dep && dep.forEach(effectFn => effectFn());
}
Signal 核心
js
function signal(initial) {
const subscribers = new Set();
let value = initial;
const read = () => {
if (activeEffect) subscribers.add(activeEffect);
return value;
};
const write = (newValue) => {
value = newValue;
subscribers.forEach(fn => fn());
};
return [read, write];
}
7.4 核心差异总结
对比项 | Vue3 响应式 | Signal |
---|---|---|
存储结构 | WeakMap → Map → Set | Signal 对象 → Set |
依赖收集范围 | 属性级别 | 值级别 |
全局性 | 依赖全局 targetMap 管理 | 每个信号独立管理 |
更新路径 | 多层查找 + 批量触发 | 直接触发订阅者 |
场景 | 全局状态、复杂组件树 | 精细化状态、局部性能优化 |
八、总结
- Signal 是一种更细粒度的响应式机制
- 相比 Vue3 的 Proxy 响应式,Signal 更新路径更短,订阅范围更小
- 在性能敏感的场景(如大列表渲染、动画状态管理)中表现更佳
- 越来越多的现代框架将 Signal 纳入核心