素材巴巴 > 程序开发 >

取消订阅RxJs Observable 的方案

程序开发 2023-09-04 09:07:00
  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。

为什么是内存泄漏

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

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

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

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

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

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

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

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

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

2. .unsubscribe()方法

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

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

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

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

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

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

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

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

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

用subscription的add方法替代subscriptions数组来收集subscription

用takeUntil声明

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

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

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

注意,我们使用了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)操作符,这是个妙招,因为它在第一次执行后自动取消订阅。

我们通过初始查询条件触发初始化查询,额外的查询由用户的交互行为触发。例如在组件模板里实现(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永远不发出通知的可能性,并考虑在组件被销毁时取消订阅。

备注—元素

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

示例:使用 <ng-container>元素展示observable流并显示它

传说中的 | async管道

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

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

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

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

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

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

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

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

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

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

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

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

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

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

来龙去脉

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

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

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

toggle()方法由用户交互行为触发

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

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

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

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

嗯嗯嗯……有办法了!

输入基于|async管道的副作用

这是啥?

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

在上述例子中,我们没有传递任何值给被调用的方法,但是这是可行的……我们可以这样做:{{doStuff(value)}}

优点

缺点

终点站—— NgRx Effects

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

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

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

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

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

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

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

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

总结

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

标签:

上一篇: AngularJS的理解模型 下一篇:
素材巴巴 Copyright © 2013-2021 http://www.sucaibaba.com/. Some Rights Reserved. 备案号:备案中。