[4+1 种方法] 如何在 Angular 中像 😎 一样取消订阅 Observable
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
介绍
在本文中,我们将了解取消订阅(又称 Observables)的一些最佳实践,以及如何保持代码的整洁和架构良好,避免内存泄漏。
简而言之,我们将使用:
- Angular
async内置管道 - RxJS 操作符(例如
takeUntil) - 自定义订阅数组
- npm
SubSink包 - npm
@ngneat/until-destroy包
问题🤔
如果你有 Angular 开发经验,你肯定知道RxJS是最强大的包之一。它使用 Observables 的概念来处理异步和事件驱动的代码。更具体地说,ObservablesObservable是一个可以随时间异步发出多个数据值的实体。
听起来很酷,对吧? 是的,它确实很酷也很强大,但你肯定已经知道了!
那么 Observables 的问题是什么呢? 内存泄漏!
让我详细解释一下,并通过一个例子来说明内存泄漏问题。下面,我们初始化了 3 个可观察对象,它们每秒发出一次值,并且我们为每个可观察对象创建了一个订阅。
@Component(/* ... */)
export class ProblematicExampleComponent implements OnInit {
constructor() {}
ngOnInit() {
interval(1000).subscribe((value) => {
console.log('sub1', value);
});
interval(1000).subscribe((value) => {
console.log('sub2', value);
});
interval(1000).subscribe((value) => {
console.log('sub3', value);
});
}
}
我们的代码似乎可以正常运行!但是,如果我们销毁这个组件(例如,导航到不包含该组件的另一个路由)会发生什么?这些值仍会继续被记录!
更糟糕的情况是,如果我们回到之前的做法,这些值会被打印两次,以此类推。如果我们创建越来越多的订阅却不进行清理,这就是典型的内存泄漏。
1.async管道
众所周知,Angular 为我们提供了许多内置功能,无需选择第三方库(毕竟,这就是我们喜欢 ❤️ Angular 的原因,对吧?)。管道(pipe)就是这样一个非常有用的功能async。
查看官方文档,我们会看到以下描述:
管道
async会订阅一个Observable或Promise,并返回它发出的最新值。当发出新值时,async管道会将组件标记为需要检查更改的状态。当组件被销毁时,async管道会自动取消订阅,以避免潜在的内存泄漏。
例子:
// async-example.component.ts
import { Component } from '@angular/core';
import { interval } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Logger } from '../utils/logger';
@Component(/* ... */)
export class AsyncExampleComponent {
private logger = new Logger(AsyncExampleComponent.name);
obs1$ = interval(1000).pipe(
tap((value) => {
this.logger.log('sub1', value);
})
);
obs2$ = interval(1000).pipe(
tap((value) => {
this.logger.log('sub2', value);
})
);
obs3$ = interval(1000).pipe(
tap((value) => {
this.logger.log('sub3', value);
})
);
constructor() {}
}
<!-- async-example.component.html -->
<p>Observable 1 value: {{obs1$ | async}}</p>
<p>Observable 2 value: {{obs2$ | async}}</p>
<p>Observable 3 value: {{obs3$ | async}}</p>
<!-- Pro Tip -->
<ng-container *ngIf="obs1$ | async as val1">
<p>Observable 1 value: {{val1}}</p>
</ng-container>
如上例所示,在创建组件期间,初始化了 3 个可观察对象,它们使用intervalRxJS 中的运算符每 1 秒发出一次值。
同时我们可以看到,这些可观察对象被传递给async了模板。这意味着它们会subscribe在初始化和unsubscribe组件销毁期间自动更新。这是一个很大的优势,因为我们无需通过代码手动操作。
2. RxJS 操作符
RxJS再次为我们提供帮助。它提供了许多有用的操作符,可以帮助我们取消订阅。
首先,让我们来看一些我们将要用到的定义:
- 主题:A
Subject是一种特殊的 Observable,它在观察者之间共享一条单一的执行路径。你可以把它想象成一个说话者在一个挤满人的房间里对着麦克风讲话。他的信息(主题)会同时传递给许多人(观察者)。这就是多播的基础。典型的 Observable 类似于一对一的对话。 - interval:一个运算符,它返回一个可观察对象,该对象根据提供的时间范围按顺序发出数字。
- takeUntil:一个过滤运算符,它会发出值,直到提供的可观察对象发出值为止。
例子:
// take-until-example.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Logger } from '../utils/logger';
@Component(/* ... */)
export class TakeUntilExampleComponent implements OnInit, OnDestroy {
private logger = new Logger(TakeUntilExampleComponent.name);
private unsubscribe$ = new Subject<void>();
constructor() {}
ngOnInit() {
interval(1000)
.pipe(takeUntil(this.unsubscribe$))
.subscribe((value) => {
this.logger.log('sub1', value);
});
interval(1000)
.pipe(takeUntil(this.unsubscribe$))
.subscribe((value) => {
this.logger.log('sub2', value);
});
interval(1000)
.pipe(takeUntil(this.unsubscribe$))
.subscribe((value) => {
this.logger.log('sub3', value);
});
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
现在让我们一步一步地详细看一下上面的例子:
- 首先,我们初始化一个新的
Subject,它不发出任何数据类型(void)。 - 然后我们借助
interval运算符创建了 3 个可观察对象。 - 我们将操作符作为管道传递给这些
takeUntil操作符,以便过滤值,直到unsubscribe$发出结果。 - 最后一步是
unsubscribe$在期间触发ngOnDestroy。我们调用.next()方法来触发新值的发出,并.complete()自动取消所有观察者的订阅。
虽然我们只用到了takeUntil运算符,但还有其他运算符也能帮到我们。例如,我们可以使用:
- `take`:它会在完成之前发出指定数量的值。如果您只对第一次发出的值感兴趣,可以使用此方法
take。例如,您可能想知道用户进入页面后点击的第一个元素是什么,或者您可能想要订阅点击事件并只获取第一次发出的值。 - takeWhile:它会一直输出值,直到提供的表达式为 false 为止。当可选的 inclusive 参数设置为 true 时,它还会输出第一个未通过谓词检验的项。
3. 自定义订阅数组
另一种取消订阅的方法Subscription是将它们放入一个数组中。这样,我们就可以遍历数组中的所有元素,并.unsubscribe()在组件销毁期间对数组中的每个元素调用取消订阅的方法。
例子:
// custom-array-example.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { interval, Subscription } from 'rxjs';
import { Logger } from '../utils/logger';
@Component(/* ... */)
export class CustomArrayExampleComponent implements OnInit, OnDestroy {
private logger = new Logger(CustomArrayExampleComponent.name);
private subs: Subscription[] = [];
constructor() {}
ngOnInit() {
const sub1 = interval(1000).subscribe((value) => {
this.logger.log('sub1', value);
});
this.subs.push(sub1);
const sub2 = interval(1000).subscribe((value) => {
this.logger.log('sub2', value);
});
this.subs.push(sub2);
const sub3 = interval(1000).subscribe((value) => {
this.logger.log('sub3', value);
});
this.subs.push(sub3);
}
ngOnDestroy() {
this.subs.forEach((s) => s.unsubscribe());
}
}
这种方法无需任何第三方库即可很好地处理多个订阅。但是,它有三个主要的缺点:
- 我们需要在组件中初始化一个额外的变量。
- 对于每个新元素,
Subscription我们都必须将其添加到数组中。 - 我们不能忘记遍历数组并取消订阅其中的项。
ngOnDestroy
4. subsinknpm 包
现在,让我们来看看一些可以帮助我们取消订阅的第三方库。更具体地说,SubSink库是一个 RxJS 订阅接收器,用于在组件中优雅地取消订阅。它的用法非常简单,您可以在下面的示例中看到它的实际应用。
例子:
// subsink-example.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { interval } from 'rxjs';
import { SubSink } from 'subsink';
import { Logger } from '../utils/logger';
@Component(/* ... */)
export class SubsinkExampleComponent implements OnInit, OnDestroy {
private logger = new Logger(SubsinkExampleComponent.name);
private subs = new SubSink();
constructor() {}
ngOnInit() {
this.subs.sink = interval(1000).subscribe((value) => {
this.logger.log('sub1', value);
});
this.subs.sink = interval(1000).subscribe((value) => {
this.logger.log('sub2', value);
});
this.subs.sink = interval(1000).subscribe((value) => {
this.logger.log('sub3', value);
});
}
ngOnDestroy() {
this.subs.unsubscribe();
}
}
与上述示例类似,我们将subs变量初始化为类的新实例SubSink。第二步是使用该sink属性,通过 setter 方法收集订阅信息。最后,我们调用该unsubscribe()方法取消所有订阅,就像在组件OnDestroy生命周期事件中一样。
如您所见,它的用法非常简单,行为与之前使用自定义订阅数组的方法类似。
5. @ngneat/until-destroynpm 包
👍 个人喜好
最后,我们将介绍@ngneat/until-destroy库。
与之前的方法类似,我们在源代码中可以非常简单地使用它。
例子:
// until-destroy-example.component.ts
import { Component, OnInit } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { interval } from 'rxjs';
import { Logger } from '../utils/logger';
@UntilDestroy()
@Component(/* ... */)
export class UntilDestroyExampleComponent implements OnInit {
private logger = new Logger(UntilDestroyExampleComponent.name);
constructor() {}
ngOnInit() {
interval(1000)
.pipe(untilDestroyed(this))
.subscribe((value) => {
this.logger.log('sub1', value);
});
interval(1000)
.pipe(untilDestroyed(this))
.subscribe((value) => {
this.logger.log('sub2', value);
});
interval(1000)
.pipe(untilDestroyed(this))
.subscribe((value) => {
this.logger.log('sub3', value);
});
}
}
在这种情况下,我们采用略有不同的方法,因为它使用了装饰器。首先,我们将@UntilDestroy()装饰器附加到组件上,然后将untilDestroyed操作符传递给 Observable 的管道。
虽然每种方案适用于不同的使用场景,但这是我最喜欢的方法,我在项目中经常使用它。我个人更喜欢安装第三方库,这样可以获得简洁易读的源代码。
结论✅
太棒了!我们终于到达终点了!🙌
希望这篇文章对您有所帮助,您可以通过选择最合适的方式取消订阅可观察对象,使您的应用程序更加简洁,避免内存泄漏问题。
我非常乐意听听您的个人喜好或提出其他建议,请在下方留言!
请用你的❤️🦄🔖支持这篇文章,帮助它传播给更多的人。🙏
另外,如果您有任何问题,请随时与我联系,您可以在这里留言或通过 Twitter 私信联系我@nikosanif。
您可以在 StackBlitz 上找到最终的源代码:
文章来源:https://dev.to/nikosanif/4-1-ways-how-to-unsubscribe-from-observables-in-angular-like-a-21f5