实现一个你自己的Signal

24 年 6 月 12 日 星期三 (已编辑)
1088 字
6 分钟

现在几乎每个 JavaScript 框架都在实现 Signal。让我们来了解:

  • Signal在底层是如何工作的
  • 为什么每个框架都采用它们
  • 如何从头实现一个Signal

JavaScript 中的响应式限制

让我们先看一个简单的例子来理解当前的限制:

javascript
let count = 0;
let double = count * 2;

count = 10;
console.log(double); // 仍然是 0,因为 double 不会自动更新

为了解决这个问题,我们可以使用函数:

javascript
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的区别。

observer.png

观察者模式的实现

基本结构:

javascript
function Observable(initialValue) {
  const subscribers = new Set();

  return {
    subscribe(fn) {
      subscribers.add(fn);
      return () => subscribers.delete(fn);
    },
    update(newValue) {
      subscribers.forEach((fn) => fn(newValue));
    },
  };
}

使用示例:

javascript
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:

javascript
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.png

Signal也有自己的订阅者,但与观察者不同,当你每次访问信号的值时,都会使用Getter和Setter,这时会自动添加订阅,这样摆脱了繁琐的订阅代码。Signal可以派生出另一个Signal,从而自动建立一张依赖关系图,当依赖项更新时,整个图都会更新。

单独的Signal并不是很有趣,它们需要搭配它们的'合作伙伴' - Reactions(响应)。Reactions(也被称为effects、autoruns、watches或computeds)会观察我们的signals并在它们的值更新时重新运行。

让我们实现一个简单的Signal

  1. 混合推送和拉取系统

signal在get时将订阅存下,在set时触发订阅回调。

javascript
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());
    },
  };
};

使用:

javascript
const count = signal(0);
  1. 副作用(Effects)

effect为signal注册副作用,副作用会立即执行,在执行期间读取signal时会触发副作用的收集。

javascript
export const effect = (fn) => {
  subscriber = fn;
  fn();
  subscriber = null;
};

使用:

javascript
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}
  1. 派生值(Derived Values)

通过副作用创建出另一个signal:

javascript
export const derived = (fn) => {
  const r = signal();
  effect(() => {
    r.value = fn();
  });
  return r;
};

使用:

javascript
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 的优势

  1. 自动依赖追踪
  • 不需要手动订阅
  • 依赖关系自动建立和清理
  1. 细粒度更新
  • 只更新真正依赖变化数据的部分
  • 自动优化性能
  1. 简化代码
  • 声明式编程
  • 减少样板代码
  • 更容易维护

参考资料

Building a Reactive Library from Scratch

TC39 Signals Proposal

文章标题:实现一个你自己的Signal

文章作者:shirtiny

文章链接:https://kizamu.anror.com/posts/your-own-signal[复制]

最后修改时间:


商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文采用CC BY-NC-SA 4.0进行许可。