unsubscribe:Angular 项目中常见场景以及是否需要 unsubscribe

本文由庄汇晔同学编写~

在 Angular 项目中,经常会使用到 observable subscribe,但是 subscribe 读取了数据之后,真的就是万事大吉了吗?这个问题的答案或许是,或许不是。有些 observable 需要 unsubscribe,而有些并不用。在接下来的文章里,我会介绍:

  • observable 的种类:何种 observable 需要 unsubscribe,以及没有 unsubscribe 会造成什么样的问题。
  • 在 angular 项目中,可能会遇到的observable 的场景,以及他们是否需要 unsubscribe,为什么需要/不需要?
  • unsubscribe 的方法。

一、observable 的种类:何种 observable 需要 unsubscribe,以及没有unsubscribe 会造成什么样的问题。

从 observable 能否结束(complete)的角度来看,observable 分为两类:finite observable(有限事件流)和 infinite observable(无限事件流)。区分他们的方法很简单:即 finite observable 只能 emit 一个值(或是一个错误),随即结束。而 infinite observable 会 emit 多个值,而不会结束。

finite Observable 的例子有:http client request,infinite Observable 的例子有:DOM eventListener。如果不取消订阅,将会出现内存泄漏,执行意料之外回调函数的问题。所以,一定需要 unsubscribe 的是 infinite observable(无限事件流),而 finite observable(有限事件流)一般不需要 unsubscribe。

二、在 angular 项目中,可能会遇到的 subscribe 的场景,他们需要 unsubscribe 吗?为什么需要/不需要?

1、Http Client 请求(this.httpClient.get(...).subscribe)

ts 复制代码
fetchFromBackend() {
  let subscription$ = this.http.get(`http://example.com`).subscribe(...)
}

是否需要 unsubscribe:视情况而定。

原因:http client 为有限事件流,当 Observable complete 后,angular 会自动关闭 Observable。由于 unsubscribe http client request 并不意味着取消请求,而是取消了返回数据后的回调函数,所以需要根据场合来取消 subscription。如果在回调函数中,如果有会造成内存泄漏的代码,则严格意义上来说,应该需要 unsubscribe

更多详情请移步阅读:medium.com/angular-in-...

2、Component 之间传递信息使用的 Behavior subject

ts 复制代码
// in service file
@Injectable({
  providedIn: 'any',
})
export class MyComponentService {
  public dataSource = new BehaviorSubject < any > (null);
  ....
}

// in component data boardcaster
this.myComponentService.dataSource.next(...)

// in component data subscriber
this.myComponentService.dataSource.subscribe(...)

是否需要 unsubscribe:需要。

原因:这种为无限事件流,无法预期它的回调函数会造成什么样的问题,所以需要 unsubscribe

3、Form control,响应式表单的变化监听(例如:this.form.valueChanges.subscribe)

ts 复制代码
initForm() {
  this.form = new FormGroup({ ...});
  this.valueChanges = this.form.valueChanges.subscribe(...);
}

是否需要 unsubscribe:需要。

原因:这种为无限事件流,虽然 component unmount 后,form 也不存在,但是这样会造成内存泄漏,导致性能下降等问题。

4、Dom 中的 fromEvent 事件监听(例如:Observable.fromEvent(this.element.nativeElement, 'click').subscribe)

ts 复制代码
@ViewChild('myElement', { static: false })
myElement: ElementRef;
constructor(private element : ElementRef) { }

click: Subscription;

ngOnInit() {
  this.click = Observable.fromEvent(this.myElement.nativeElement, 'click').subscribe(...);
}

是否需要 unsubscribe:需要。

原因:这种为无限事件流,虽然 component unmount 后,dom element 不存在,但是这样会造成内存泄漏,导致性能下降等问题。

5、router 变化事件(例如:this.route.queryParams.subscribe)

ts 复制代码
constructor(private route: ActivatedRoute, private router: Router) {
  this.route.params.subscribe(console.log);
  this.route.queryParams.subscribe(console.log);
  this.route.fragment.subscribe(console.log);
  this.route.data.subscribe(console.log);
  this.route.url.subscribe(console.log);
  
  this.router.events.subscribe(console.log);

}

是否需要 unsubscribe:需要。

原因:这种为无限事件流,如果不 unsubscribe,会运行错误的回调函数,造成不可控的问题

三、unsubscribe 的方法

1,使用 rxjs takeUntil,takeWhile,take(n),first operator:

简介:rxjs take 系列的 operator 可以限定取得 Observable emit 的次数或者时机,当次数或时机不符合时,Observable 就会走向完成状态。所以,在 angular 项目中,可以灵活利用组件的生命周期函数,使得组件需要 unsubscribe 的 Observable 在组件销毁时,自动完成。first operator 则和 take(1)类似,是只取得第一个 emit 值,随后完成。

takeUntil: rxjs.dev/api/index/f...

takeUntil(notifier: ObservableInput)会一直监听他的数据源,直到 notifier Observable emit 一个值

ts 复制代码
import { Subject, takeUntil } from 'rxjs';

export class Foo implements OnInit {
  private destroy$ = new Subject();
  constructor() { }

  ngOnInit(): void {
    this.auth.fireAuthUser$
      .pipe(takeUntil(this.destroy$))
      .subscribe({
        next: (data) => console.log(data),
        complete: () => this.uponComplete(),
      });
  }

  uponComplete() {
    console.log('Finally completed');
  }

  ngOnDestroy() {
    this.destroy$.next(true);
    this.destroy$.complete();
  }
}

takeWhile: rxjs.dev/api/index/f...

与 takeUnitl 类似,只是 takeUnitl 的参数是一个判断函数,而 takeWhile 的参数是一个 Observable export class ViewRouteComponent implements OnInit, OnDestroy { alive: boolean = true

ts 复制代码
constructor(private titleService: TitleService) {
  ngOnInit() {
    this.titleService.emitter1$
      .pipe(takeWhile(() => this.alive))
      .subscribe((data: any) => { /_ ... do something 1 _/ })
  }

  ngOnDestroy() {
    this.alive = false
  }
}

take(n): rxjs.dev/api/index/f...

在取得 n 次 emit 之后,Observable 会自动完成

ts 复制代码
@Component({
  ...
  })
export class AppComponent implements OnInit {
  subscription$
  ngOnInit() {
    let observable$ = Rx.Observable.interval(1000);
    this.subscription$ = observable$.pipe(take(1)).
      subscribe(x => console.log(x))
  }
}

first: rxjs.dev/api/index/f...

在取得第一次 emit 后,Observable 自动完成

ts 复制代码
@Component({
  ...
  })
export class AppComponent implements OnInit {
  subscription$
  ngOnInit() {
    let observable$ = Rx.Observable.interval(1000);
    this.subscription$ = observable$.pipe(first()).
      subscribe(x => console.log(x))
  }
}

2,使用 unsubscribe 方法

使用 unsubscribe 方法是非常直观的一种方式,只需在 angular 组件的 ngOnDesrtoy 中调用即可。

这里的例子使用了一个 subscription array,在 ngOnDestroy 中 unsubscribe

ts 复制代码
export class AppComponent implements OnInit, OnDestroy {
  subscription1$: Subscription
  subscription2$: Subscription
  subscriptions: Subscription[] = []
  ngOnInit() {
    var observable1$ = Rx.Observable.interval(1000);
    var observable2$ = Rx.Observable.interval(400);
    this.subscription1$ = observable1$.subscribe(x =>
      console.log("From interval 1000", x)
    );
    this.subscription2$ = observable2$.subscribe(x =>
      console.log("From interval 400", x)
    );
    this.subscriptions.push(this.subscription1$);
    this.subscriptions.push(this.subscription2$);
  }
  ngOnDestroy() {
    this.subscriptions.forEach((subscription) =>
      subscription.unsubscribe()
      );
  }
}

3,使用 async pipe

官方文档:angular.io/api/common/...

使用这种方式可以无需手动 unsubscribe,也不用担心内存泄漏的问题。但是它的局限性也在于,它只能被使用在模版(template)中。请考虑如下场景:在用户点击按钮后,需要提交一个表单。此时,使用 async pipe 则变得不怎么合适了。

ts 复制代码
@Component({
  selector: 'async-observable-pipe',
  template: '<div><code>observable|async</code>: Time: {{ time | async }}</div>'
})
export class AsyncObservablePipeComponent {
  time = new Observable < string > ((observer: Observer<string>) => {
    setInterval(() => observer.next(new Date().toString()), 1000);
  });
}

补充:如何查看组件销毁后,是否存在 unsubscribe 的 subscription?

方法 1:从静态代码检查中规避

使用 eslint 工具,配置 rxjs-no-ignored-subscription 规则:github.com/cartant/esl...

方法 2:使用堆快照查看内存泄漏

例子中想要查看内存泄漏的组件是 MonthlyReportComponent,此时使用开发者工具,在加载组件前使用快照功能,并搜索组件名称,是看不到内存泄漏的

在组件加载后,unmount,再次捕捉快照,再次搜索组件名称,查看堆快照:

查看里面是否有 BehaviorSubject,Observable 等未取消订阅的对象

参考文档:

http client:

stackoverflow.com/questions/3...

stackoverflow.com/questions/3...

medium.com/angular-in-...

如何 unsubscribe:

stackoverflow.com/questions/4...

blog.bitsrc.io/6-ways-to-u...

benlesh.medium.com/rxjs-dont-u...

stackoverflow.com/questions/6...

stackoverflow.com/questions/3...

如何查看存在 unsubscribe:

itnext.io/angular-rxj...

关于 OpenTiny

OpenTiny 是一套企业级 Web 前端开发解决方案,提供跨端、跨框架、跨版本的 TinyVue 组件库,包含基于 Angular+TypeScript 的 TinyNG 组件库,拥有灵活扩展的低代码引擎 TinyEngine,具备主题配置系统TinyTheme / 中后台模板TinyPro/ TinyCLI命令行等丰富的效率提升工具,可帮助开发者高效开发 Web 应用。


欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~更多视频内容也可关注B站、抖音、小红书、视频号

OpenTiny 也在持续招募贡献者,欢迎一起共建

OpenTiny 官网opentiny.design/
OpenTiny 代码仓库github.com/opentiny/
TinyVue 源码github.com/opentiny/ti...
TinyEngine 源码github.com/opentiny/ti...

欢迎进入代码仓库 Star🌟TinyEngineTinyVueTinyNGTinyCLI~ 如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

相关推荐
是立不是利25 分钟前
写给设计师的 CSS 博客美学指南
前端·css
默_笙1 小时前
🏝 Docker 就是"房地产开发":从施工图纸到小区物业的容器化指南
前端·javascript
计算机魔术师1 小时前
Rohan Paul 谈用时间跨度衡量智能体能力,并引用 OpenAI 自动化研究实习生里程碑
前端
峰向AI1 小时前
告别 API 费用! 这个开源工具让你的 AI 助手在本地运行
github
kyriewen1 小时前
我手写了个极简版 React Router——才搞懂 v6 为什么砍了这么多 API
前端·javascript·程序员
a1117761 小时前
Shirone 二次元博客主题 开源
前端·开源
IT_陈寒1 小时前
Python的切片赋值把我坑惨了,这不是bug是特性
前端·人工智能·后端
掘金酱2 小时前
【社区公告】致每一位掘友:关于这次调整, 想再说几句
前端·人工智能
CoderLiu2 小时前
程序化工具调用(PTC)与动态工作流引擎:深入大模型工具调用的架构演进与实践
前端·人工智能·后端
码农胖大海2 小时前
我的第一个产品,只有一段提示词
前端·ai编程·产品