信号使 Angular 变得更容易
为什么 RxJS 不能做所有事情?
RxJS 很棒,但它也有局限性。考虑使用“服务中的主题”方法的简单变体实现的计数器:
export class CounterService {
count$ = new BehaviorSubject(0);
increment() {
this.count$.next(this.count$.value + 1);
}
}
现在多个组件可以通过订阅来共享和响应此状态count$
:
现在让我们使用 RxJSmap
和combineLatest
运算符添加一些派生状态:
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."))
);
以下是这些反应关系的图表:
它看起来是这样的:
这难道不简单吗?RxJS 帮我们搞定了一切。这应该没什么问题。
其实是有的。让我们在map
for 语句中放入一个控制台日志message$
,看看当我们增加一次计数时会发生什么。
message$ = this.over9000$.pipe(
map((over9000) => {
console.log('Calculating message$', over9000);
return over9000 ? "It's over 9000!" : "It's under 9000.";
})
);
为什么要运行 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
。
我们必须学习更多运算符来处理这些问题:map
与distinctUntilChanged
(有时带比较器)、combineLatest
与debounceTime
、和shareReplay
。实际上,它们不是shareReplay
,更像是publishReplay
与refCount
。或者实际上是merge
,,NEVER
和(稍后会详细介绍)。真正疯狂的是,大多数人甚至都没有意识到所有这些问题。只有经历过一些痛苦的经历,才能认识到这些运算符share
的ReplaySubject
必要性。
但是,要求每个人都避免RxJS 的诸多陷阱,熟悉订阅的工作原理,并学习所有这些操作符,而仅仅是为了基本的派生状态,这实在是荒谬的。而且,这些操作符会增加包的大小,并且在运行时执行操作。创建自定义操作符并不能解决这个问题。
因此,虽然 RxJS 在管理异步事件流方面非常出色,但它在同步状态方面效率低下且难以使用。
选择器怎么样?
选择器在计算派生状态方面非常高效。
但我从来不喜欢它们的语法:
createSelector(
selectItems,
selectFilters,
(items, filters) => items.filter(filters),
);
因此,对于StateAdapt,我想出了新的语法:
buildAdapter<State>(...)({
filteredItems: s => s.items.filter(s.filters),
})();
但是选择器需要一个具有全局状态对象的状态管理库,这使得它们无法与框架 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."
);
现在当我们点击时,我们会得到 5 个预期的日志:
它甚至比优化的 RxJS 更高效,并且我们只需要一个“操作符”:computed
。
Angular 团队在实现方面也做得非常出色。如果你想了解更多关于它的工作原理,我推荐你看看他们与 Ryan Carniato 的访谈。
信号问题
信号很棒,但与 RxJS 一样,它们也有局限性:
- 异步反应性
- 急切状态和陈旧状态
这些将是我下一篇文章的主题。
鏂囩珷鏉ユ簮锛�https://dev.to/mfp22/signals-make-angular-much-easier-3k9