取消订阅RxJs Observable 的方案
- .subscribe()方法,为“内存泄漏”代言
- .unsubscribe()方法
- 用takeUntil声明
- 用take(1)进行初始化。
- 传说中的 | async管道
- 换个思路——| async管道太长了
- 终点站—— 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接口。这本身可能来自两种错误。
- 忘记实现OnDestroy接口本身、
- 忘记在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永远不发出通知的可能性,并考虑在组件被销毁时取消订阅。
备注—元素
在我们进一步探讨之前,先聊一聊
示例:使用 <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意味着我们正在使用更加良好的理念处理副作用,这使体系结构更清洁,促进可维护性,并且更容易测试!
总结
- RxJS是管理异步事件集合的强大工具
- 我们订阅的事件流可以发出0到多个值,甚至可能是无限的
- 这使得我们需要手动取消订阅
- 内存泄漏是未能正确的取消订阅导致的结果
- 在Angular中,有很多方法可以取消对Observable流的订阅
- 不同的方法为我们提供了不同的权衡
- 通常,最好使用|async管道来订阅和解析组件模板中的值(借助
元素) - |async管道也会触发副作用,但是有一种更好的方法
- 使用NgRx Effects来响应Observable流触发的副作用!
标签:
相关文章
-
无相关信息