迄今为止,已经写了 很多 文章 , 您需要取消订阅 Observable RxJS订阅,否则会发生内存泄漏 。 对于大多数此类文章的读者来说,坚定的规则是“签名?-签名!”。 但是,不幸的是,此类文章中的信息经常会失真或无法协商,甚至在替换概念时更糟。 我们将讨论这个。

以这篇文章为例: https : //medium.com/ngx/why-do-you-need-unsubscribe-ee0c62b5d21f

当他们对我说“ 可能会降低生产率的机会”时,我立即想到了过早的优化 。


我们继续阅读该人的昵称,是Reactive Fox :


此外,还有有用的信息和提示。 我同意您应该始终退订RxJS中无尽的流 。 但我只关注有害信息(我认为)。

哇...赶上了恐怖。 目前这种缺乏事实根据的威胁(没有指标,数字……)导致了这样一个事实,即对于大量前端用户而言,缺乏取消订阅就像是公牛的一块红布。 当他们偶然发现这一点时,除了这块破布,他们再也看不到其他东西了。

文章的作者甚至进行了演示应用程序,试图证明他的想法:
https://stackblitz.com/edit/why-you-have-to-unsubscribe-from-observable-material
确实,在他的立场上,您可以看到处理器如何执行不必要的工作(当我没有单击任何东西时)以及内存消耗如何增加(小的变化):

为了确认您始终需要取消订阅Observable HttpClient请求这一事实,他添加了一个请求拦截器,该请求拦截器在控制台中显示“仍然活跃...仍然活跃...仍然活跃...”:

即 这个人截取了最后的数据流,将其设为无限 (在发生错误的情况下,会重复请求,但错误总是会发生),并以此作为证据,表明您需要退订最终的数据流。
StackBlitz不太适合测量应用程序性能,因为 升级过程中会自动进行同步,并且会占用资源。 所以我做了测试应用程序: https : //github.com/andchir/test-angular-app
那里有两个窗户。 当您打开每个文件时,都会将请求发送到action.php ,其中模仿了非常耗费资源的操作,其中有3秒的延迟。 而且action.php会将所有请求记录到log.txt文件中。
Action.php代码<?php header('Content-Type: application/json'); function logging($str, $fileName = 'log.txt') { if (is_array($str)) { $str = json_encode($str); } $rootPath = __DIR__; $logFilePath = $rootPath . DIRECTORY_SEPARATOR . $fileName; $options = [ 'max_log_size' => 200 * 1024 ]; if (!is_dir(dirname($logFilePath))) { mkdir(dirname($logFilePath)); } if (file_exists($logFilePath) && filesize($logFilePath) >= $options['max_log_size']) { unlink($logFilePath); } $fp = fopen( $logFilePath, 'a' ); $dateFormat = 'd/m/YH:i:s'; $str = PHP_EOL . PHP_EOL . date($dateFormat) . PHP_EOL . $str; fwrite( $fp, $str ); fclose( $fp ); return true; } $actionName = isset($_GET['a']) && !is_array($_GET['a']) ? $_GET['a'] : '1'; logging("STARTED-{$actionName}"); sleep(3);
但是首先是一个小题外话。 在下面的图片(可单击)中,您可以看到一个简单的示例,说明JavaScript垃圾收集器如何在Chrome浏览器中工作。 发生了PUSH,但是setTimeout并未阻止垃圾收集器清除内存。

实验时,请不要忘记按一下按钮来调用垃圾收集器。

让我们回到我的测试应用程序。 这是两个窗口的代码:
BadModalComponent代码 @Component({ selector: 'app-bad-modal', templateUrl: './bad-modal.component.html', styleUrls: ['./bad-modal.component.css'], providers: [HttpClient] }) export class BadModalComponent implements OnInit, OnDestroy { loading = false; largeData: number[] = (new Array(1000000)).fill(1); destroyed$ = new Subject<void>(); data: DataInterface; constructor( private http: HttpClient, private bsModalRef: BsModalRef ) { } ngOnInit() { this.loadData(); } loadData(): void { // For example only, not for production. this.loading = true; const subscription = this.http.get<DataInterface>('/action.php?a=2').pipe( takeUntil(this.destroyed$), catchError((err) => throwError(err.message)), finalize(() => console.log('FINALIZE')) ) .subscribe({ next: (res) => { setTimeout(() => { console.log(subscription.closed ? 'SUBSCRIPTION IS CLOSED' : 'SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('LOADED'); this.data = res; this.loading = false; }, error: (error) => { setTimeout(() => { console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('ERROR', error); }, complete: () => { setTimeout(() => { console.log(subscription.closed ? 'COMPLETED - SUBSCRIPTION IS CLOSED' : 'COMPLETED - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('COMPLETED'); } }); } close(event?: MouseEvent): void { if (event) { event.preventDefault(); } this.bsModalRef.hide(); } ngOnDestroy() { console.log('DESTROY'); this.destroyed$.next(); this.destroyed$.complete(); } }
如您所见,有一个取消订阅(takeUntil)。 “老师”所建议的一切。 还有很多。
GoodModalComponent代码 @Component({ selector: 'app-good-modal', templateUrl: './good-modal.component.html', styleUrls: ['./good-modal.component.css'] }) export class GoodModalComponent implements OnInit, OnDestroy { loading = false; largeData: number[] = (new Array(1000000)).fill(1); data: DataInterface; constructor( private http: HttpClient, private bsModalRef: BsModalRef ) { } ngOnInit() { this.loadData(); } loadData(): void { // For example only, not for production. this.loading = true; const subscription = this.http.get<DataInterface>('/action.php?a=1').pipe( catchError((err) => throwError(err.message)), finalize(() => console.log('FINALIZE')) ) .subscribe({ next: (res) => { setTimeout(() => { console.log(subscription.closed ? 'SUBSCRIPTION IS CLOSED' : 'SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('LOADED'); this.data = res; this.loading = false; }, error: (error) => { setTimeout(() => { console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('ERROR', error); }, complete: () => { setTimeout(() => { console.log(subscription.closed ? 'COMPLETED - SUBSCRIPTION IS CLOSED' : 'COMPLETED - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('COMPLETED'); } }); } close(event?: MouseEvent): void { if (event) { event.preventDefault(); } this.bsModalRef.hide(); } ngOnDestroy() { console.log('DESTROY'); } }
具有大数组的属性完全相同,但没有答复。 这并不能阻止我称这个窗口为好窗口。 为什么-稍后。
观看视频:
如您所见,在两种情况下,切换到第二个组件后,垃圾收集器都成功地将内存恢复为正常值。 是的,关闭窗口后也可以清除内存,但这在我们的实验中并不重要。 事实证明,“老师”说的是错误的:
例如,您提出了一个请求,但是当答案尚未从后端到达时,您将不必要地破坏该组件,那么您的订阅将保留该组件的链接 ,从而造成潜在的内存泄漏。
是的,他在谈论“潜在”泄漏。 但是,如果流是有限的,那么就不会有内存泄漏。
我预见到这种“老师”的愤慨感叹。 他们肯定会告诉我们这样的消息: “好的,没有内存泄漏,但是通过取消订阅我们也取消了请求 ,这意味着我们将确保在收到服务器的响应后不再执行任何代码 。 ” 首先,我并不是说退订总是不好的,我只是说您正在替换概念 。 是的,答案到达后将执行更多无用的操作的事实是不好的,但是您只能通过取消订阅 (在这种情况下)来保护自己免受实际内存泄漏的影响 ,并且可以通过其他方式保护自己免受其他不良影响 。 无需恐吓读者并强加他们自己的代码编写风格。
如果用户改变主意,我们是否总是需要取消请求? 并非总是如此! 不要忘记您取消了该请求, 但不要取消服务器上的操作 。 假设用户打开了一个组件,很长时间以来一直在加载某些东西,而他正在切换到另一个组件。 服务器可能已加载并且无法处理所有请求和操作。 在这种情况下,由于请求不会在服务器端停止 (在大多数情况下),因此用户可以疯狂地导航中的所有链接并在服务器上创建更大的负载 。
观看以下视频:
我让用户等待答案。 在大多数情况下,答案很快就会到来,并且不会给用户带来任何不便。 但是通过这种方式,我们将避免服务器执行重复的繁重操作(如果有)。
总结:
- 我并不是说您不需要取消订阅HttpClient请求的RxJS订阅。 我只是说有时候没有必要。 无需替换概念。 如果您正在谈论内存泄漏,请显示此泄漏。 不是您无尽的console.log ,即泄漏。 内存量是多少? 手术时间是多少? 这是需要显示的。
- 我没有将我在测试应用程序中应用的解决方案称为“银弹”。 相反,我敦促给予追随者更多的自由。 让他决定如何编写他的代码。 无需恐吓他并强加自己的发展风格。
- 我反对狂热主义和过早的优化。 我最近看到的太多了。
- 该浏览器具有比我展示的方法更高级的查找内存泄漏的方法。 我认为这种简单方法的应用就足够了。 但我建议您更详细地熟悉该主题,例如,在本文中: https : //habr.com/en/post/309318/ 。
UPD#1
目前,该职位下降了将近一天。 起初他去了利弊,然后评估从零开始。 这意味着观众被精确地分为两个阵营。 我不知道这是好是坏。
UPD#2
在评论中,出现了Jet Fox(正在分析的文章的作者)。 起初他感谢我,他很有礼貌。 但是,看到观众的消极情绪,他开始施压。 他说我应该道歉。 即 他撒了谎(谎言在上面用黄色框勾勒出),我应该道歉。
最初,我认为他在演示应用程序中编写的具有无限重复(很好的重复2-3次)的流的拦截器仅用于测试和通知。 但事实证明,他认为他是生活中的榜样。 即 阻塞窗口的按钮- 这是不可能的 。 并创建此类拦截器,违反SOLID原则,违反应用程序的模块化(模块和组件必须彼此独立),让您的单元(组件,服务)的单元测试通过林-您可以。 想象一下情况:您编写了一个组件,并为其编写了单元测试。 然后出现了这样的Fox,在您的应用程序中添加了类似的拦截器,并且您的测试变得无用。 然后他仍然对您说:“您为什么不预测我可能要添加这样的拦截器。那么,请更正您的代码。” 也许在他的团队中这可能是现实,但是我不认为应该鼓励或对此视而不见。
UPD#3
这些评论主要讨论订阅和退订。 是否有一个名为“退订邪恶”的帖子? 不行 我不敦促您不要退订。 像以前一样做。 但是您必须了解为什么要这样做。 退订不是过早的优化。 但是,踏上防范潜在威胁的道路(正如所分析文章的作者所说的那样),您可以越界。 然后,您的代码可能变得超载并且难以维护。
这篇文章是关于狂热的,它导致未验证信息的分发。 在某些情况下,有必要更加冷静地解决取消订阅的问题(您需要清楚地了解特定情况下是否存在问题)。
UPD#4
相反,我敦促给予追随者更多的自由。 让他决定如何编写他的代码。
在这里您需要澄清。 我追求标准。 但这标准可以由图书馆的作者或他的团队来设置,而事实并非如此(在文档和正式文件中)。 例如,Symfony框架文档的“ 最佳实践”部分。 如果RxJS文档中的内容相同并且会显示“ signed-unsubscribe”,那么我就不想与他争论。
UPD#5
重要评论以及知名人士的回答:
https://habr.com/cn/post/479732/#comment_21012620
RxJS开发人员提出的履行“签署-取消订阅”合同的建议是存在的 ,但这是非正式的。