现在几乎每个 JavaScript 框架都在实现 Signal。让我们来了解:
- Signal在底层是如何工作的
- 为什么每个框架都采用它们
- 如何从头实现一个Signal
JavaScript 中的响应式限制
让我们先看一个简单的例子来理解当前的限制:
let count = 0;
let double = count * 2;
count = 10;
console.log(double); // 仍然是 0,因为 double 不会自动更新
为了解决这个问题,我们可以使用函数:
let count = 0;
const double = () => count * 2;
count = 10;
console.log(double()); // 现在是 20
count = 20;
console.log(double()); // 现在是 40
但这比较麻烦,因为每次改变count的值,都需要手动去执行double(),我们需要自动响应值的变化。
观察者模式(Observer Pattern)
为了解决上述问题,人们提出了观察者模式。观察者模式是一种行为设计模式,它:
- 允许定义一种订阅机制
- 可以通知多个对象关于被观察对象上发生的事件
为降低初学者的学习门槛,在Signal之前,有必要先实现Observer,以此来了解Observer与Signal的区别。

观察者模式的实现
基本结构:
function Observable(initialValue) {
const subscribers = new Set();
return {
subscribe(fn) {
subscribers.add(fn);
return () => subscribers.delete(fn);
},
update(newValue) {
subscribers.forEach((fn) => fn(newValue));
},
};
}
使用示例:
const count = Observable(0);
const unsubscribe1 = count.subscribe((value) => {
console.log(`这是第1个订阅 value: ${value}`);
});
const unsubscribe2 = count.subscribe((value) => {
console.log('这是第2个订阅');
});
count.update(5);
// 输出: 这是第1个订阅 value: 5
// 输出: 这是第2个订阅
unsubscribe1(); // 取消订阅1
这看起来很好,但有一些繁琐,每次我们都需要自己去设置这些订阅。而且如果有第二个值,比如double:
const count = Observable(0);
const double = Observable(0);
// 需要手动设置依赖关系
count.subscribe((value) => {
double.update(value * 2);
});
// 如果还有 triple 值
const triple = Observable(0);
count.subscribe((value) => {
triple.update(value * 3);
});
可以看到,如果你想在count改变时,去修改double或者做其他的事情时,依赖关系需要手动维护,容易遗漏更新,代码也会变得更加难懂。 如果忘记清理某个订阅,还可能导致内存泄漏。
观察者模式的更多细节参考:Observer Pattern
Signal
Signal 也是一种Observable对象也被称为atom、subject或者refs。

Signal也有自己的订阅者,但与观察者不同,当你每次访问信号的值时,都会使用Getter和Setter,这时会自动添加订阅,这样摆脱了繁琐的订阅代码。Signal可以派生出另一个Signal,从而自动建立一张依赖关系图,当依赖项更新时,整个图都会更新。
单独的Signal并不是很有趣,它们需要搭配它们的'合作伙伴' - Reactions(响应)。Reactions(也被称为effects、autoruns、watches或computeds)会观察我们的signals并在它们的值更新时重新运行。
让我们实现一个简单的Signal
- 混合推送和拉取系统
signal在get时将订阅存下,在set时触发订阅回调。
let subscriber = null;
export const signal = (value) => {
const sets = new Set();
return {
get value() {
subscriber && sets.add(subscriber);
return value;
},
set value(v) {
value = v;
sets.forEach((cb) => cb());
},
};
};
使用:
const count = signal(0);
- 副作用(Effects)
effect为signal注册副作用,副作用会立即执行,在执行期间读取signal时会触发副作用的收集。
export const effect = (fn) => {
subscriber = fn;
fn();
subscriber = null;
};
使用:
const button = {};
const count = signal(0);
effect(() => {
button.text = count.value;
console.log('render button:', button);
});
const click = () => {
count.value++;
};
click(); // 输出 render button:{text: 1}
click(); // 输出 render button:{text: 2}
- 派生值(Derived Values)
通过副作用创建出另一个signal:
export const derived = (fn) => {
const r = signal();
effect(() => {
r.value = fn();
});
return r;
};
使用:
const button = {};
const count = signal(0);
const double = derived(() => count.value * 2);
const click = () => {
count.value++;
};
effect(() => {
console.log('count.value', count.value);
});
effect(() => {
button.text = double.value;
console.log('render button:', button);
});
click(); // 输出 count.value 1 \n render button:{text: 2}
click(); // 输出 count.value 2 \n render button:{text: 4}
Signal 的优势
- 自动依赖追踪
- 不需要手动订阅
- 依赖关系自动建立和清理
- 细粒度更新
- 只更新真正依赖变化数据的部分
- 自动优化性能
- 简化代码
- 声明式编程
- 减少样板代码
- 更容易维护