angular promise请求_【译】Angular应用中,取消订阅Rxjs Observable 的最佳实践!

3b20f219482342b772b7b7487b2194b8.png
原文链接: The Best Way To Unsubscribe RxJS Observables In The Angular Applications!
原文作者:Tomas Trajan

译者写在开头

本文可能涉及的相关名词,这些名词不适合直接翻译:

Observable (可观察对象) Observer (观察者) Subscription (订阅) Operators (操作符) Subject (主体) Schedulers (调度器)

具体可查看文档 :概览 | RxJS 中文文档。

原文下面有几条评论很有含金量推荐大家看一看。

相关推荐:

郑丰彧:[译] RxJS: 别取消订阅


对Rxjs Observables的简介

(可跳过此部分,直接进入正题)

Rxjs(也称Observables-S)是一项前端领域的新兴技术。它在Angular的核心API中被大量使用了,也随Angular水涨船高。经证明它是一个处理异步事件集合的强力工具。

我们可以将RxJS Observable 看作是一个潜在的无限异步数组,它可以方便地进行filter、map等诸多操作

异步编程思想的发展史

最开始,我们使用简单的回调函数。我们将回调函数作为参数传递给其他函数,当其他函数完成后,回调函数随后被调用。

6ae9a1ec17ce9df86629244c166f5cbc.png

后来,大家更愿意使用promise。promise承诺返回单个值来作为其结果或是错误。一旦该返回值被确立,处理程序就会被触发。尘埃落定,继续前行。

然而,一切都被改变了。因为前端的大牛们开始进一步揭秘推/拉的谜团...

基于Observable ,我们现在可以处理0到多个值。

Observables需要新的方法来消费传入的值。我们必须订阅observable流,每当一个新值被发出,我们的处理器就会被通知。

我们无法提前知道会有多少值。甚至,一些流可能是无限的(例如用户单击、websocket信息)。因而我们必须主动控制订阅。

在Angular应用中,有很多不同的方法来处理RxJS订阅

这些方法在简洁性、健壮性和易读性方面提供了不同的权衡。本文中,我们将探索多种解决方案,并会尽量进行优化,使它:

  • 简洁——减少代码量
  • 健壮——尽量规避bug
  • 易读——易于理解

我们将学到什么?

在下文中,我们将用各种方案来订阅RxJs Observable。

  1. .subscribe()方法,为“内存泄漏”代言
  2. .unsubscribe()方法
  3. 用takeUntil声明
  4. 用take(1)进行初始化。
  5. 传说中的 | async管道
  6. 换个思路——| async管道太长了
  7. 终点站—— NgRx Effects

1. .subscribe()方法,为“内存泄漏”代言

让我们从最简单的例子开始。我们有一个timer,它是一个无限的冷模式Observable。

冷模式Observable本身什么也不做。必须有人订阅它才能开始执行。无限意味着一旦Observable被订阅就永远不会complete。

我们在组件的ngOnInit 方法中对timer进行订阅,每当timer发出新的值都调用console.log。

22c59925c3a36370a174c47a1a047462.png

为什么是内存泄漏

上述实现看起来很好,完全符合我们的期望。假如我们导航到了使用其他的组件的页面,将会发生什么?

组件被销毁,但Subscription依然存在。

更多的日志将被添加到浏览器的控制台中。不仅如此,假如我们回到原来的路由。该组件将被重新创造并伴随着新的订阅。

我们如若多次重复这个过程,控制台输出会非常繁忙。

什么情况下可以只进行订阅

在Angular应用启动时仅被实例一次的某些组件(如AppComponent)与大多数服务(除了来自懒加载模块和@Component装饰器中提供的服务)。

在上述情况下,可以对Observable进行订阅并且不取消订阅。

这些组件和服务将在整个应用程序生命周期中存在,因此不会产生任何内存泄漏。

最终,当我们从应用程序跳转到其他网站时,这些订阅将被清理。

2. .unsubscribe()方法

好吧,我们也许不小心导致了一些内存泄漏并且迫切的想要摆脱它们!

内存泄漏是销毁-重建组件时没有清理现有订阅时产生的。随着我们重建组件不断地添加订阅,因而导致内存泄漏……

订阅一个Observable可得到一个拥有unsubscribe()方法的Subscription对象 ,该方法可以用来取消不需要的订阅。

0af13dd8b3b88d054013f0ebe1f1ffdd.png
存一个subscription ,在ngOnDestory方法中对其执行unsubscribe ,ngOnDestory在组件销毁时被调用。

亦或是想象一下多个subscription的场景......

b665db97984069aa938e3566f86e71b5.png
在subscriptions 数组里存多个subscription 变量,在ngOnDestroy中全部执行unsubscribe

这不对吗?不不不,它运行得很好。问题在于是我们将observable流与普通的命令式逻辑混合在了一起。

根据我的经验,RxJS的初学者真的需要将命令式编程思维转换为 流式编程思维。虽说采用命令式替代‘对Observable更为友好的’声明式更容易学习,但应该避免这样的行为。

感谢Wojciech Trawiński通过展示Subscription本身有一种内部机制来强调上述观点。尽管如此,我依然建议使用更具声明式的方法来取消订阅……

9c99def2392a72a26d203b5bde745182.png
用subscription的add方法替代subscriptions数组来收集subscription

用takeUntil声明

好的,继续前行。我们通过使用takeUntil操作符优化程序(Rxjs创始人——Ben Lesh很乐意这样做)。

官方文档:takeUntil(notifier: Observable<any>)——直到作为通知者的Observable发出值,源Observable才发出若干值。

(译者:这个说法比较拗口,请参考takeUntil · 学习 RxJS 操作符)

a17f351ce791fc432e0e54152eff7b93.png
注意,我们使用了Observable的.pipe()方法来添加操作符,在我们的例子里是将takeUntil添加到Observable链中。

这种解决方案是声明式的!这意味着,需要把适配整个生命周期的一切都搞定之前就把Observable链声明好了。

takeUntil()很棒,但不幸的是,它也有一些缺点

最明显的是,它相当啰嗦! 我们必须创建额外的Subject,并在应用中的每个组件中都正确实现OnDestroy接口,相当之多!

更大的问题是,这是一个非常容易出错的过程。非常容易忘记实现OnDestroy接口。这本身可能来自两种错误。

  1. 忘记实现OnDestroy接口本身、
  2. 忘记在ngOnDestroy实现中的.next()和.complete()方法(将其置为空方法)

最大的问题是这两件事不会导致任何明显的错误,它们很容易被忽略!

实现自定义(或者找到已存在的)tslint规则可以解决这个问题,该规则将为每一个组件校验有没有正确实现ngOnDestroy。这样依然有问题,因为不是每个组件都使用了subscription。

非常感谢Brian Love的回复 我们不应该忘记takeUntil应为管道中的最后一个操作符(通常),以防止其后的操作符会返回阻止清理的额外observable。

用take(1)进行初始化。

有些subscription只在程序启动的时候发生一次。它们可能启动一些处理或者触发第一个请求来加载初始化数据。

在这种场景下,我们可以使用RxJS take(1)操作符,这是个妙招,因为它在第一次执行后自动取消订阅。

6f630e922e7021bbee97cb4e5d0211f0.png
我们通过初始查询条件触发初始化查询,额外的查询由用户的交互行为触发。例如在组件模板里实现(onchange)=”searchResult($event.target.value)

take(n: number)操作符可以传入任何数字,在我们的场景下只需要1。

请记住,假如原有observable从不发出数据,take(1)便不会被触发(也不会使observable流complete)。我们必须确保在不发生这种情况下才使用它,否则还得提供额外的取消订阅处理!感谢Brian Love的回复。

顾名思义,也可以用first操作符。此外,该操作符支持传入判断方法,有点像filter与take(1)的结合。

译者:这一节没讲清楚,请移步到原文看Brian Love的回复,节选如下:
Brian Love:我还想指出,在使用first()/take(1)方法时,您需要小心观察observable永远不发出通知的可能性,并考虑在组件被销毁时取消订阅。

备注—<ng-container>元素

在我们进一步探讨之前,先聊一聊<ng-container>元素。这是个很特别的元素,它从不产生任何DOM。这使得它成为用于模板条件判断的完美工具,下面我们将非常方便实现这样的模板。

fa5c92c03707d135a4ad8aae88d87168.png
示例:使用 &amp;amp;amp;lt;ng-container&amp;amp;amp;gt;元素展示observable流并显示它

传说中的 | async管道

Angular自带了对管道功能的支持。管道可以对多种多样的数据转换清晰的抽象,并复用在多个组件的模板里。

| json管道是一个好例子,它是开箱即用,允许我们显示Javascript对象的内容。我们可以在这样的模板中使用它:{{someObject | json}}。

Angular有| async管道,该管道"偷偷地"订阅Observable对象,并提供解析后的、单纯的、常规的Javascript值。然后可以像往常一样在模板中使用这个值。

不仅如此,当组件被销毁时,|async管道发起的所有订阅都会自动取消。这是一种完美的方案,我们可以很容易地使用异步数据,而且不可能导致内存泄漏!

当组件被销毁时,|async管道自动取消所有订阅。

29d1408c13603e7f872e00e346f220bf.png
使用 |async管道 在模板中解析todoState 流的示例

*ngIf结合|async管道和的另一个巨大优势是,我们可以保证在呈现子组件时,所有子组件都可以使用被解析后的值。

这种方法可以帮助我们避免在模板中过度使用“elvis”操作符,以避免prop of undefined的错误……没有<ng-container>,它看起来更像这样……

译者注:elvis不是个Rxjs操作符,实在不知道怎么翻译,看wiki吧......Elvis operator - Wikipedia

aa31d0c8dbf3d7306c70a746042b81fb.png
“elvis” 的应用,?. operator避免“prop of undefined” 的错误

换个思路——| async管道太长了

正如一句有趣的谚语所说……

科学家们过于专注于如何让它起运行,以至于忘了自省这么做到底有没有意义……

在我使用Angular NgRx Material Starter时发生了这种情况我企图移除每一个OnDestroy/takeUntil。我想出了一个有趣的解决办法,但真的不推荐它。不管怎样,看看再说

来龙去脉

上文使用了|async解决方案可以完美地用于任何下述情况:当我们需要获取并在UI中显示Observable数据。

这非常好,被解析后的数据可以在模板中使用,我可以自由进行展示或者传递到组件方法中。唯一缺少的是上述方法的触发(调用)。

通常,这将是我们的用户及其与组件的交互的责任。假设我们想切换单个todo项…

65d1450176e38b1d149e77dcd2455739.png
toggle()方法由用户交互行为触发

被解析后的数据可用于模板,也可将其作为用户交互的结果传递给todoService 。

当我们需要触发一些东西作为对数据本身的响应时,又该如何呢?我们不能指望用户为我们做这些事儿。这非常适合使用.subscribe(),对吧?

ab80c948f09590acd33a8176fb9a5db0.png
使用subscribe设置新的浏览器标题作为对导航的响应

但是我们的目标是不使用.subscribe(),至少不需要手动取消订阅……

嗯嗯嗯……有办法了!

928d876ba91c5c9e6d64d743de121909.png
输入基于|async管道的副作用

这是啥?

我们使用|async管道订阅Observable,每次Observable流推入新值时,我们用{{ }}计算(执行)组件的updateTitle()方法,而不是显示任何内容。

在上述例子中,我们没有传递任何值给被调用的方法,但是这是可行的……我们可以这样做:<ng-container *ngIf="someStream$ | async as value">{{doStuff(value)}}</ng-container>。

优点

  • 更少的代码和概念(没有Subject、.next()和.complete(),没有OnDestroy,也没有takeUntil)
  • 不会忘记实现(或者错误运用)OnDestory/takeUntil
  • 订阅总是发生在模板(本地)中

缺点

  • 必须与OnPush变更检测策略一起使用(否则它将在每个变更检测周期中调用该函数)
  • 很诡异(笔者的主观想法)
  • 这样使用可能是错的
  • 可能会有更好的方案...

终点站—— NgRx Effects

在上面解决方案中,我们试图借助|async管道在组件模板之外实现一些功能。外面发生的事情,或者说“在一边”,听起来很像一个指向副作用的暗示。

在这篇文章中,我们主要讨论的是普通的RxJS,但是Angular生态系统也包含了NgRx:一个基于RxJS的状态管理库,它实现了单向数据流(Flux / Redux模式)

NgRx Effects可以帮助我们从应用程序中删除最后的显式.subscribe()调用(不需要基于模板的副作用)!Effects是独立实现的,由库自动订阅。

让我们看一个例子,看看实现起来是什么样的……

681000fffa60b973a3dcf2d022342f2d.png
NgRx Effects的实现。这仍然需要向EffectsModule.forFeature([TitleEffects…])导入到一些NgModule。

Observable的操作流(或任何其他流)将由库订阅和管理,因此我们不必实现任何取消订阅的逻辑。乌拉 !

在NgRx Effects的帮助下实现的副作用独立于组件的生命周期,它可以防止内存泄漏和一堆其他问题!

作为一个额外的好处,使用NgRx Effects意味着我们正在使用更加良好的理念处理副作用,这使体系结构更清洁,促进可维护性,并且更容易测试!

总结

  1. RxJS是管理异步事件集合的强大工具
  2. 我们订阅的事件流可以发出0到多个值,甚至可能是无限的
  3. 这使得我们需要手动取消订阅
  4. 内存泄漏是未能正确的取消订阅导致的结果
  5. 在Angular中,有很多方法可以取消对Observable流的订阅
  6. 不同的方法为我们提供了不同的权衡
  7. 通常,最好使用|async管道来订阅和解析组件模板中的值(借助<ng-container>元素)
  8. |async管道也会触发副作用,但是有一种更好的方法
  9. 使用NgRx Effects来响应Observable流触发的副作用!

今天就到这里!

希望您喜欢这篇文章,现在能够轻松地在Angular应用程序中处理订阅了!

永远不要忘记,未来是光明的

7def8632fbcb530f2f1a1878536795bc.png
光明的未来( by Vitalii Ustymenko)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值