使用Http方法创建的可观察对象是否需要进行退订?

359

你是否需要取消订阅 Angular 2 的 HTTP 请求以防止内存泄漏?

 fetchFilm(index) {
        var sub = this._http.get(`http://example.com`)
            .map(result => result.json())
            .map(json => {
                dispatch(this.receiveFilm(json));
            })
            .subscribe(e=>sub.unsubscribe());
            ...

1
可能是如何在Angular 2中防止内存泄漏?的重复问题。 - Eric Martinez
1
请参考https://dev59.com/ZlsW5IYBdhLWcg3w66qH(以及评论)来防止Angular 2中的内存泄漏。 - Eric Martinez
2
如果您正在阅读这个问题,请注意接受的答案(由提问者自己编写)可能存在问题。如果您阅读其他答案,它们都反驳了您不必取消订阅的观点。仅仅因为一个答案得到了最多的赞同并被接受,这并不意味着它是正确的。 - Liam
11个回答

357
所以答案是否定的,不需要你自己清理。 Ng2会自动清理它。
来自Angular Http XHR后端源代码的Http服务源代码:

enter image description here

请注意它在获取结果后运行complete()。这意味着它实际上在完成时取消订阅。所以您不需要自己这样做。
以下是一个验证测试:
  fetchFilms() {
    return (dispatch) => {
        dispatch(this.requestFilms());

        let observer = this._http.get(`${BASE_URL}`)
            .map(result => result.json())
            .map(json => {
                dispatch(this.receiveFilms(json.results));
                dispatch(this.receiveNumberOfFilms(json.count));
                console.log("2 isUnsubscribed",observer.isUnsubscribed);
                window.setTimeout(() => {
                  console.log("3 isUnsubscribed",observer.isUnsubscribed);
                },10);
            })
            .subscribe();
        console.log("1 isUnsubscribed",observer.isUnsubscribed);
    };
}

作为预期,您可以看到在获取结果并完成可观察操作后,它总是自动取消订阅。这发生在超时(#3)上,因此我们可以在所有操作完成并完成时检查可观察对象的状态。
而结果是:

enter image description here

因此,由于Ng2自动取消订阅,不会存在任何泄漏!

值得一提的是:这个Observable被归类为有限,与无限Observable相反,后者是可以像DOM click监听器一样无限地发出数据流。

感谢@rubyboy的帮助。


50
如果用户在收到响应之前离开页面,或者在用户离开页面之前未收到响应,会发生什么情况?这不会导致信息泄露吗? - Thibs
15
答案始终是取消订阅。这个答案完全错误,原因就是这个评论提到的。用户导航离开是一个内存链接。而且,如果在用户导航离开后,那个订阅中的代码运行,可能会导致错误/产生意外的副作用。我倾向于在所有可观察对象上使用takeWhile(()=>this.componentActive),并在ngOnDestroy中设置this.componentActive=false来清理组件中的所有可观察对象。 - Lenny
10
这里展示的例子涉及深度的角度私有 API。与其他私有 API 一样,它可能会在无通知的情况下随着升级而发生更改。经验法则是:如果没有正式文件记录,就不要做任何假设。例如,AsynPipe 有清晰的文档说明它会为您自动订阅/取消订阅。HttpClient 文档中没有提到这一点。 - YoussefTaghlabi
1
@YoussefTaghlabi,FYI,官方的 Angular 文档(https://angular.io/guide/http)确实提到了:“AsyncPipe 会自动订阅(和取消订阅)”。因此,这是官方文件中明确记录的。 - Hudgi
1
@Lenny 我看到很多类似的建议,即使是针对http请求也是如此。但我不明白订阅http请求的Observable怎么会导致内存泄漏。据我所知,XmlHttpRequest只在等待服务器响应期间由浏览器持有。收到响应后,XmlHttpRequest和Observable就会从堆栈中分离出来,因此可以进行垃圾回收处理。那我们为什么要担心内存泄漏呢?P.S. 我知道这与在已销毁的组件上执行响应回调没有任何关系。 - Eugene Khudoy
显示剩余3条评论

214

你们在说什么啊!!!

好的,有两个理由可以取消订阅任何可观察对象。但很少有人谈论到非常重要的第二个原因!

  1. 清理资源。正如其他人所说,这对于HTTP可观察对象来说是一个微不足道的问题。它只会自己清理。
  1. 防止运行subscribe处理程序。

请注意,对于HTTP可观察对象,在取消订阅时,它将为您取消浏览器中的底层请求(您将在网络面板中看到“已取消”以红色显示)。这意味着不会浪费时间下载或解析响应。但那实际上是我的下面主要观点的一件小事。

第二个原因的相关性将取决于您的subscribe处理程序的操作:

如果您的subscribe()处理程序函数具有任何不需要的副作用,那么当调用它的任何内容关闭或处置时,您必须取消订阅(或添加条件逻辑),以防止其被执行。

考虑一些情况:

  1. 一个登录表单。您输入用户名和密码,然后点击“登录”。如果服务器很慢,您决定按Esc关闭对话框怎么办?您可能会认为您没有登录,但是如果http请求在您按下Esc后返回,那么您仍将执行其中的任何逻辑。这可能导致重定向到帐户页面,设置不想要的登录cookie或令牌变量。这可能不是您的用户预期的结果。

  2. 一个“发送电子邮件”表单。

如果“sendEmail”的subscribe处理程序执行类似于触发“您的电子邮件已发送”动画、将您转移到另一页或尝试访问任何已被处理的内容,则可能会出现异常或意外行为。

此外,要小心不要假设unsubscribe()的意思是“取消”。一旦HTTP消息正在传输中,unsubscribe()将无法取消已经到达您的服务器的HTTP请求。它只会取消返回给您的响应。而邮件可能仍然会被发送。

如果您在UI组件内直接创建订阅以发送电子邮件,则可能需要在处理完毕时取消订阅,但如果电子邮件是由非UI集中式服务发送的,则可能不需要。

    一个 Angular 组件被销毁/关闭时,任何仍在运行的 http 观察者将会完成并运行其逻辑,除非你在onDestroy()中取消订阅。后果轻重取决于你在 subscribe 处理程序中所做的事情。如果您尝试更新不再存在的内容,则可能会出现错误。 有时您可能会希望在组件被处理时执行某些操作,有些则不需要。例如,也许您为已发送电子邮件设置了“嗖”声音。即使组件已关闭,您可能仍想播放此声音,但如果您尝试在组件上运行动画,它将失败。在这种情况下,在 subscribe 中添加一些额外条件逻辑是解决方案 - 您不需要取消订阅 http 观察者。 因此,实际答案是,不需要取消订阅以避免内存泄漏。但是,您需要(经常)这样做以避免由运行可能引发异常或破坏应用程序状态的代码触发不需要的副作用。 提示:Subscription 包含一个 closed 布尔属性,在高级情况下可能会有用。对于 HTTP 来说,当它完成时,这个值将被设置。在 Angular 中,对于某些情况,将 _isDestroyed 属性设置在 ngDestroy 中可能非常有用,它可以在 subscribe 处理程序中进行检查。

    提示2: 如果要处理多个订阅,您可以创建一个临时的new Subscription()对象,并将任何其他订阅add(...) 到它上面-这样当您从主订阅中取消订阅时,它也会取消订阅所有添加的订阅。


2
还要注意,如果您有一个返回原始HTTP可观察对象的服务,并且在订阅之前进行了管道处理,则只需要从最终可观察对象取消订阅,而不是底层的HTTP可观察对象。实际上,您甚至没有直接针对HTTP的订阅,因此也无法取消它。 - Simon_Weaver
即使取消订阅会取消浏览器请求,服务器仍会执行该请求。有没有一种方法可以告知服务器也中止操作?我正在快速连续发送 HTTP 请求,其中大部分都是通过取消订阅来取消的。但服务器仍在处理客户端取消的请求,导致合法请求等待。 - bala
1
@bala,你需要自己想出一个机制来实现这个功能,这与 RxJS 没有任何关系。例如,你可以将请求放入表格中,在后台等待 5 秒钟后再运行它们,如果需要取消某些请求,只需删除或设置旧行上的标志即可停止它们的执行。这完全取决于你的应用程序是什么。但是,由于你提到服务器正在阻塞,可能配置为一次只允许一个请求 - 但这也取决于你使用的是什么。 - Simon_Weaver
5
技巧2 - 是一项专业技巧,适用于想要通过调用一个函数来取消多个订阅的人。使用 .add() 方法添加,然后在 ngDestroy 中使用 .unsubscribe()。 - abhay tripathi
2
对于那些在2022年及以后来到这里的人,更加专业的提示是使用Net Basal的https://github.com/ngneat/until-destroy包。你可以稍后感谢我。 - AsGoodAsItGets

71

必须要取消订阅,如果您希望在所有网络速度下都获得确定性的行为。

想象一下,组件A被呈现在选项卡中——您单击按钮发送一个“GET”请求。响应需要200毫秒才能返回。所以您可以随时安全地关闭选项卡,知道机器比您更快,HTTP响应已经被处理并且在选项卡关闭后组件A已经销毁了。

那么,在网络很慢的情况下怎么办呢?您单击按钮,'GET'请求花费10秒钟才能收到它的响应,但是5秒钟后你决定关闭选项卡。这将会销毁组件A并在以后被垃圾回收。等一下!我们没有取消订阅——现在 5秒钟后,响应返回,已销毁组件中的逻辑将被执行。该执行现在被认为是上下文外的,可能导致许多问题,包括非常低的性能和数据/状态损坏。

因此,最佳实践是在组件被销毁时使用takeUntil()并取消订阅HTTP呼叫订阅。

注意:

  • RxJS不是特定于Angular的
  • Angular中用于模板的async管道会自动在销毁时取消订阅
  • 多次取消订阅没有负面影响,除了额外的no-op调用
import { Component, OnInit, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

interface User {
  id: string;
  name: string;
  age: number;
}

@Component({
  selector: 'app-foobar',
  templateUrl: './foobar.component.html',
  styleUrls: ['./foobar.component.scss'],
})
export class FoobarComponent implements OnInit, OnDestroy {
  private user: User = null;
  private destroy$ = new Subject();

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.http
      .get<User>('api/user/id')
      .pipe(takeUntil(this.destroy$))
      .subscribe(user => {
        this.user = user;
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();  // trigger the unsubscribe
    this.destroy$.complete(); // finalize & clean up the subject stream
  }
}

1
ngOnDestroy() 中,使用 BehaviorSubject() 并仅调用 complete() 是否可行? - Saksham
6
不需要从HttpClient observables取消订阅,因为它们是有限的observables,即在发出值后会完成。 - Yatharth Varshney
2
无论如何,您都应该取消订阅。假设存在一个拦截器来弹出错误消息,但您已经离开了触发错误的页面...您会在另一个页面中看到错误消息...此外,请考虑一个健康检查页面,其中调用所有微服务...等等。 - Washington Guedes
也可以尝试使用异步管道 :D,这是自动订阅和取消订阅的推荐方式。 - Rebai Ahmed
将其变成肌肉记忆 - 在 .ts 文件中每次看到 .subscribe(),请确保有一个明确的取消订阅。即使使用了 take(n)first() 等方法也要如此。单个“上下文不清”的执行可能会导致专家团队困惑数周! - Avid Coder
显示剩余6条评论

30

经过一段时间的测试、阅读文档和HttpClient源代码。

HttpClient:https://github.com/angular/angular/blob/master/packages/common/http/src/client.ts

HttpXhrBackend :https://github.com/angular/angular/blob/master/packages/common/http/src/xhr.ts

HttpClientModule:https://indepth.dev/exploring-the-httpclientmodule-in-angular/

Angular University:https://blog.angular-university.io/angular-http/

这种类型的Observables是单值流:如果HTTP请求成功,这些Observables将仅发出一个值,然后完成

而整个“我需要”取消订阅的问题的答案取决于你的回调函数中的逻辑。

这要看情况。 Http调用内存泄漏不是一个问题。 问题在于回调函数中的逻辑。

例如:路由或登录。

如果您的调用是一个登录调用,则不需要“取消订阅”,但您需要确保如果用户离开页面,则在缺乏用户时正确处理响应。


this.authorisationService
      .authorize(data.username, data.password)
      .subscribe((res: HttpResponse<object>) => {
          this.handleLoginResponse(res);
        },
        (error: HttpErrorResponse) => {
          this.messageService.error('Authentication failed');
        },
        () => {
          this.messageService.info('Login has completed');
        })

从烦人到危险

现在想象一下,网络比平常慢,通话需要长达5秒钟,用户离开登录视图并进入“支持视图”。

组件可能没有激活,但订阅仍在进行中。如果有响应,用户将会突然重定向(取决于您的handleResponse()实现)。

这是不好的

还要想象一下,用户离开电脑,认为他还没有登录。但是您的逻辑已经将用户登录,现在您面临安全问题。

在不取消订阅的情况下,您可以做什么?

使您的调用依赖于视图的当前状态:

  public isActive = false;
  public ngOnInit(): void {
    this.isActive = true;
  }

  public ngOnDestroy(): void {
    this.isActive = false;
  }

使用.pipe(takeWhile(value => this.isActive)),以确保只有在视图处于活动状态时才处理响应。


this.authorisationService
      .authorize(data.username, data.password).pipe(takeWhile(value => this.isActive))
      .subscribe((res: HttpResponse<object>) => {
          this.handleLoginResponse(res);
        },
        (error: HttpErrorResponse) => {
          this.messageService.error('Authentication failed');
        },
        () => {
          this.messageService.info('Login has completed');
        })

但是你如何确定订阅不会导致内存泄漏?

您可以记录是否应用了“teardownLogic”。

当订阅为空或取消订阅时,将调用订阅的teardownLogic。


this.authorisationService
      .authorize(data.username, data.password).pipe(takeWhile(value => this.isActive))
      .subscribe((res: HttpResponse<object>) => {
          this.handleLoginResponse(res);
        },
        (error: HttpErrorResponse) => {
          this.messageService.error('Authentication failed');
        },
        () => {
          this.messageService.info('Login has completed');
    }).add(() => {
        // this is the teardown function
        // will be called in the end
      this.messageService.info('Teardown');
    });


你不必取消订阅。你应该知道,如果你的逻辑存在问题,这可能会导致订阅出现问题。并对其进行处理。在大多数情况下,这不是问题,但特别是在关键任务(如授权)中,您应该注意意外行为,无论是使用“取消订阅”还是其他逻辑(如管道或条件回调函数)。 为什么不总是取消订阅? 想象一下,您发出了一个put或post请求。服务器都会接收到消息,只是响应需要一段时间。取消订阅不会撤销put或post请求。
但是,当您取消订阅时,您将没有机会处理响应或通知用户,例如通过对话框或Toast / Message等。这会导致用户认为put / post请求未完成。
所以这取决于你。这是你的设计决策,如何处理这些问题。

28

调用unsubscribe方法相当于取消正在进行的HTTP请求,因为该方法在底层XHR对象上调用abort方法并删除负载和错误事件的侦听器:

// From the XHRConnection class
return () => {
  _xhr.removeEventListener('load', onLoad);
  _xhr.removeEventListener('error', onError);
  _xhr.abort();
};

话虽如此,unsubscribe会移除监听器...所以这可能是个好主意,但我不认为对于单个请求是必要的 ;-)

希望能帮到你, Thierry


这是荒谬的,我一直在寻找停止的方法......但我在想,如果是我编码,在某些情况下:我会这样做:y = x.subscribe((x)=>data = x); 然后用户更改输入和 x.subscribe((x)=>cachlist[n] = x); y.unsubscribe() 嘿,你使用了我们的服务器资源,我不会抛弃你.....而在另一种情况下:只需调用 y.stop() 把一切都扔掉。 - Hassan Faghihi
Thierry Templier,感谢您的建议。但是假设在某种情况下,在页面加载时我必须进行10个API调用,但在数据加载完成之前我导航到另一个页面,那么这将非常有用(取消订阅将取消所有API调用,这对性能非常有帮助)。 - moh

14

新的HttpClient模块中,与原来相同的行为依然保持不变。 packages/common/http/src/jsonp.ts


以上代码似乎来自单元测试,这暗示着使用 HttpClient 时,你需要自己调用 .complete() 方法。 - CleverPatrick
是的,你是对的,但如果你检查(https://github.com/angular/angular/blob/4c2ce4e8ba4c5ac5ce8754d67bc6603eaad4564a/packages/common/http/src/jsonp.ts#L159),你会看到相同的情况。关于主要问题,是否需要明确取消订阅?在大多数情况下不需要,因为 Angular 会自己处理。可能存在一种情况,即长时间响应可能发生,并且您已经转移到另一个路由或者销毁了组件,在这种情况下,您有可能尝试访问不再存在的内容,并引发异常。 - Ricardo Martínez

13

您一定要阅读这篇文章,它向您展示了为什么即使是http请求也应该始终取消订阅

如果在从后端收到答案之前创建请求但销毁了组件,那么您的订阅将保留对组件的引用,从而导致可能会引起内存泄漏的机会。

更新

上述说法似乎是正确的,但无论如何,在收到答案时,http订阅都会被销毁。


2
是的,你实际上会在浏览器的网络选项卡中看到一个红色的“取消”按钮,这样你就可以确定它有效。 - Simon_Weaver
链接已损坏。 - robert
@robert 是的,现在似乎有问题。 - Mateut Alin

6

在自动完成的可观察对象(例如 Http 调用)中,您不应该取消订阅。但是对于无限可观察对象,如 Observable.timer(),则需要取消订阅。


1
一个有助于理解的好方法是,除非调用subscribe函数,否则不会进行HTTP请求。虽然本页上的答案似乎表明了两种不同的做法,但实际上并没有太大区别,因为所需的行为将由异步管道控制,正如Angular docs中所示(尽管它在“Making a DELETE request”一节中被提及得更晚):
AsyncPipe会自动为您订阅(和取消订阅)。
事实上,在文档中很难找到任何示例,其中这些可观察对象通过调用取消订阅函数而明确取消订阅。

0
是的,有必要取消订阅。
根据Angular文档

当组件被销毁时,您应该始终取消订阅可观察对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接