信号使 Angular 变得更容易

2025-06-09

信号使 Angular 变得更容易

YouTube

为什么 RxJS 不能做所有事情?

RxJS 很棒,但它也有局限性。考虑使用“服务中的主题”方法的简单变体实现的计数器:



export class CounterService {
  count$ = new BehaviorSubject(0);

  increment() {
    this.count$.next(this.count$.value + 1);
  }
}


Enter fullscreen mode Exit fullscreen mode

现在多个组件可以通过订阅来共享和响应此状态count$

图片描述

现在让我们使用 RxJSmapcombineLatest运算符添加一些派生状态:



  count$ = new BehaviorSubject(1000);

  double$ = this.count$.pipe(map((count) => count * 2));
  triple$ = this.count$.pipe(map((count) => count * 3));

  combined$ = combineLatest([this.double$, this.triple$]).pipe(
    map(([double, triple]) => double + triple)
  );

  over9000$ = this.combined$.pipe(map((combined) => combined > 9000));

  message$ = this.over9000$.pipe(
    map((over9000) => (over9000 ? "It's over 9000!" : "It's under 9000."))
  );


Enter fullscreen mode Exit fullscreen mode

以下是这些反应关系的图表:

图片描述

它看起来是这样的:

共享计数器状态 2

这难道不简单吗?RxJS 帮我们搞定了一切。这应该没什么问题。

其实是有的。让我们在mapfor 语句中放入一个控制台日志message$,看看当我们增加一次计数时会发生什么。



  message$ = this.over9000$.pipe(
    map((over9000) => {
      console.log('Calculating message$', over9000);
      return over9000 ? "It's over 9000!" : "It's under 9000.";
    })
  );


Enter fullscreen mode Exit fullscreen mode

图片描述

为什么要运行 4 次?我们只增加了一次计数。这效率不高。

发生了一些奇怪的事情。让我们把控制台日志放在每个可观察对象中,这样我们就可以查看所有发生的事情。想一想我们应该期待什么。我们有一个事件和5个派生状态:double$triple$combined$over9000$message$。我们不应该看到5个控制台日志吗?好吧,我们实际得到的是这样的:

图片描述

超过 9000 条了!!!我们只是用最简单的方式实现了这个功能,这就是 RxJS 给我们带来的。这有 40 条日志,是实际数量的 8 倍。

我们需要了解订阅的工作原理。我们有两个组件订阅了几个这样的可观察对象。在这里,我为每个订阅添加了一条彩色线:

图片描述

每个订阅都会一路传递到链的顶端。如果算上 和 旁边的蓝线和绿线的数量double$triple它们各有 8 条。这就是每条线对应的控制台日志数量。combined$周围有 12 行(因为有分支),还有 12 条日志。 但是message$有 2 行,而不是 2 条,而是 4 条控制台日志;over9000$有 4 行,但有 8 条控制台日志。这是因为每条线最终都在 处分成了 2 行combineLatest

我们必须学习更多运算符来处理这些问题:mapdistinctUntilChanged(有时带比较器)、combineLatestdebounceTime、和shareReplay。实际上,它们不是shareReplay,更像是publishReplayrefCount。或者实际上是merge,,NEVER和(稍后会详细介绍)。真正疯狂的是,大多数人甚至都没有意识到所有这些问题。只有经历过一些痛苦的经历,才能认识到这些运算符shareReplaySubject必要性。

但是,要求每个人都避免RxJS 的诸多陷阱,熟悉订阅的工作原理,并学习所有这些操作符,而仅仅是为了基本的派生状态,这实在是荒谬的。而且,这些操作符会增加包的大小,并且在运行时执行操作。创建自定义操作符并不能解决这个问题。

因此,虽然 RxJS 在管理异步事件流方面非常出色,但它在同步状态方面效率低下且难以使用。

选择器怎么样?

选择器在计算派生状态方面非常高效。

但我从来不喜欢它们的语法:



createSelector(
  selectItems,
  selectFilters,
  (items, filters) => items.filter(filters),
);


Enter fullscreen mode Exit fullscreen mode

因此,对于StateAdapt,我想出了新的语法:



buildAdapter<State>(...)({
  filteredItems: s => s.items.filter(s.filters),
})();


Enter fullscreen mode Exit fullscreen mode

但是选择器需要一个具有全局状态对象的状态管理库,这使得它们无法与框架 API(例如组件输入)紧密集成。

信号

Angular 需要自己的反应原语,在所有选项中,信号是同步的最佳选择。

让我们用 Angular 信号来实现我们的计数器:



  count = signal(1000);

  double = computed(() => this.count() * 2);
  triple = computed(() => this.count() * 3);

  combined = computed(() => this.double() + this.triple());

  over9000 = computed(() => this.combined() > 9000);

  message = computed(() =>
    this.over9000() ? "It's over 9000!" : "It's under 9000."
  );


Enter fullscreen mode Exit fullscreen mode

现在当我们点击时,我们会得到 5 个预期的日志:

图片描述

它甚至比优化的 RxJS 更高效,并且我们只需要一个“操作符”:computed

Angular 团队在实现方面也做得非常出色。如果你想了解更多关于它的工作原理,我推荐你看看他们与 Ryan Carniato 的访谈。

信号问题

信号很棒,但与 RxJS 一样,它们也有局限性:

  1. 异步反应性
  2. 急切状态和陈旧状态

这些将是我下一篇文章的主题。

鏂囩珷鏉ユ簮锛�https://dev.to/mfp22/signals-make-angular-much-easier-3k9
PREV
使用 2 个 shadcn/ui 组件构建可扩展/可折叠数据表
NEXT
10 个最有用的 IDE 热键及 GIF 示例 [第二部分]