메모리 누수 방지

26611 단어 rxjsangular
https://www.bampouris.eu/blog/avoid-memory-leaks-angular
약 5년 전에 벤 레쉬는 아주 좋은 글을 썼는데 제목은 RxJS: Don’t Unsubscribe이었다.물론 저자는 우리에게 자신의 삶에 영원히 관심을 갖지 말라고 말하지 않았다Subscription.그의 말은 우리가 모든 과정에서 수동으로 집행할 필요가 없다는 것을 찾아야 한다는 것이다. .unsubscribe()우리의 임무를 시작합시다!

우리의 노선도


일부 전역 구성 요소(예를 들어 AppComponent)의 생존 기간은 응용 프로그램 자체의 생존 기간과 같다.만약 우리가 이러한 상황을 처리하고 있다는 것을 알았다면 .subscribe() 메모리 유출 보호 절차를 제공하지 않은 상황에서 관찰할 수 있는 대상을 관찰할 수 있다.그러나 모든 개발자에게 각도 응용 프로그램의 실현 과정에서 메모리 유출을 처리하는 것은 관건적인 임무이다.우리는 메모리 유출의 의미를 보여주기 위해 탐색을 시작할 것이다. 우리는 먼저 .unsubscribe()의 '전통적인' 방식으로 이 문제를 해결하고 우리가 더 좋아하는 모델을 탐색할 것이다.
  • The Bad Open Subscriptions
  • Unsubscribe the Old Way
  • The Async Pipe
  • The RxJS Operators
  • The DestroyService
  • Conclusions
  • 엉터리 공개 구독


    두 개의 라우팅 구성 요소 FirstComponentSecondComponent (첫 번째 Cmp와 두 번째 Cmp 내비게이션 링크 단추) 를 포함하는 간단한 프레젠테이션 프로그램이 있습니다.FirstComponent(경로/first에 대응하는) 구독 관찰 가능timer1$, ScreenMessagesComponent를 통해MessageService에 메시지를 발송한다.정보가 화면 하단에 표시됩니다.
    Live Example
    export class FirstComponent implements OnInit {
      timer1$ = timer(0, 1000);
    
      constructor(private messageService: MessageService) {}
    
      ngOnInit(): void {
        this.timer1$.subscribe((val) =>
          this.messageService.add(`FirstComponent timer1$: ${val}`)
        );
      }
    }
    
    우리가 /second 경로를 내비게이션했을 때 FirstComponent는 이미 파괴되었다.그러나 우리는 상술한 구독에서 전해진 소식을 여전히 볼 수 있다.이것은 우리가 '닫힌 문' 을 잊어버렸기 때문이다. 우리의 응용 프로그램에는 오픈Subscription이 하나 있다.우리가 왔다 갔다 하면서 점점 더 많은 구독을 추가함에 따라, 이 구독들은 앱이 닫힐 때만 닫힌다.우리는 메모리 유출을 처리해야 한다.

    구형 구독 해지


    상술한 문제를 해결하는 간단한 방법은 실현lifecycle hook방법ngOnDestroy()이다.공식 문서에서 보듯이

    ...Unsubscribe Observables and detach event handlers to avoid memory leaks...


    export class FirstComponent implements OnInit, OnDestroy {
      private timer1$ = timer(0, 1000);
    
      private subscription: Subscription;
    
      constructor(private messageService: MessageService) {}
    
      ngOnInit(): void {
        this.subscription = this.timer1$.subscribe((val) =>
          this.messageService.add(`FirstComponent timer1$: ${val}`)
        );
      }
    
      ngOnDestroy(): void {
        this.subscription.unsubscribe();
      }
    }
    

    그 밖에 우리가 여러 개Subscription가 있다면 우리는 모든 사람을 위해 똑같은 일을 해야 한다.
    export class FirstComponent implements OnInit, OnDestroy {
      private timer1$ = timer(0, 1000);
      private timer2$ = timer(0, 2500);
    
      private subscription1: Subscription;
      private subscription2: Subscription;
    
      constructor(private messageService: MessageService) {}
    
      ngOnInit(): void {
        this.subscription1 = this.timer1$.subscribe((val) =>
          this.messageService.add(`FirstComponent timer1$: ${val}`)
        );
    
        this.subscription2 = this.timer2$.subscribe((val) =>
          this.messageService.add(`FirstComponent timer2$: ${val}`)
        );
      }
    
      ngOnDestroy(): void {
        this.subscription1.unsubscribe();
        this.subscription2.unsubscribe();
      }
    }
    
    만약 우리가 구독이 하나 또는 두 개 없고 .unsubscribe() 호출 수를 줄이려고 한다면, 우리는 아버지 Subscription 를 만들고 아들 Subscription 을 추가할 수 있습니다.부모 구독을 취소하면 그 안에 추가된 모든 하위 구독도 구독을 취소합니다.
    Live Example
    export class FirstComponent implements OnInit, OnDestroy {
      private timer1$ = timer(0, 1000);
      private timer2$ = timer(0, 2500);
    
      private subscription = new Subscription();
      constructor(private messageService: MessageService) {}
    
      ngOnInit(): void {
        this.subscription.add(
          this.timer1$.subscribe((val) =>
            this.messageService.add(`FirstComponent timer1$: ${val}`)
          )
        );
    
        this.subscription.add(
          this.timer2$.subscribe((val) =>
            this.messageService.add(`FirstComponent timer2$: ${val}`)
          )
        );
      }
    
      ngOnDestroy(): void {
        this.subscription.unsubscribe();
      }
    }
    
    부모.unsubscribe()를 사용하면 우리는 많은 속성에 관심을 가질 필요가 없다. 우리도 하나만 실행한다AsyncPipe.

    비동기 파이프


    AsyncPipe 대박!구성 요소의 템플릿에 '반응식' 으로 데이터를 표시하려고 할 때, 그 데이터는 적수가 없습니다.

    The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes. When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks.


    Live Example
    @Component({
      selector: 'app-first',
      template: `
        <p>first component works!</p>
        <p>{{ timer3$ | async }}</p>
      `,
    })
    export class FirstComponent implements OnInit, OnDestroy {
      ...
    
      timer3$ = timer(0, 1000);
    
      ...
    }
    
    .subscribe()를 사용하려면 수동.unsubscribe() 또는 takeUntil이 필요하지 않습니다.

    RxJS 연산자


    RxJS는 관찰 시퀀스로 비동기적이고 이벤트 기반의 프로그램을 작성하는 라이브러리입니다.그것은 다음과 같은 훌륭한 운영사들이 있다.
  • take
  • takeWhile
  • first
  • last
  • 우리는 그들 모두의 입장에 서지 않을 것이다.우리는 takeUntil 연산자의 용법만 볼 것이다.

    Lets values pass until a second Observable, notifier, emits a value. Then, it completes.


    우선, 나는 본문에서 묘사한 위험을 언급하고 싶다. RxJS: Avoiding takeUntil Leaks.pipe 조작원은 반드시 destroy$ 중의 마지막 조작원이어야 한다.

    If the takeUntil operator is placed before an operator that involves a subscription to another observable source, the subscription to that source might not be unsubscribed when takeUntil receives its notification.


    Live Example
    export class FirstComponent implements OnInit, OnDestroy {
      ...
      private destroy$ = new Subject<void>();
    
      constructor(private messageService: MessageService) {}
    
      ngOnInit(): void {
        this.timer1$
          .pipe(takeUntil(this.destroy$))
          .subscribe(
            (val) => this.messageService.add(`FirstComponent timer1$: ${val}`),
            (err) => console.error(err),
            () => this.messageService.add(`>>> FirstComponent timer1$ completed`)
          );
    
        this.timer2$
          .pipe(takeUntil(this.destroy$))
          .subscribe(
            (val) => this.messageService.add(`FirstComponent timer2$: ${val}`),
            (err) => console.error(err),
            () => this.messageService.add(`>>> FirstComponent timer2$ completed`)
          );
      }
    
      ngOnDestroy(): void {
        this.destroy$.next();
        this.destroy$.complete();
      }
    }
    
    여기Observable는 우리의 두 번째ngOnDestroy()(통지 프로그램)로 내부complete()의 생명주기 갈고리를 보내고 이런 방식으로 데이터 흐름의 완성을 촉발한다.이런 방법의 장점 중 하나는 실제로 관찰할 수 있는 것을 완성했기 때문에 .unsubscribe() 리셋을 호출하는 것이다.우리가 전화ngOnDestroy()를 했을 때, 우리는 예약 취소 통지를 받을 수 없습니다.

    결점


    상기 모든 해결 방안은 실제적으로 우리의 문제를 해결했지만 적어도 하나의 단점이 있다. 우리는 모든 구성 요소에서 자신의 일을 반복해서 실현해야 한다takeUntil.샘플 파일을 더 줄일 수 있는 더 좋은 방법이 있습니까?네, 저희는 ngOnDestroy()Angular's DI mechanism를 이용할 것입니다.

    서비스 제거


    Live Example
    우선 FirstComponent 한 서비스로 이동합니다.
    import { Injectable, OnDestroy } from '@angular/core';
    import { Subject } from 'rxjs';
    
    @Injectable()
    export class DestroyService extends Subject<void> implements OnDestroy {
      ngOnDestroy() {
        this.next();
        this.complete();
      }
    }
    
    DestroyService 서비스 인스턴스(공급자 메타데이터 배열을 통해)를 제공하고 그 구조 함수를 통해 해당 인스턴스를 자신에게 주입합니다.
    @Component({
      selector: 'app-first',
      template: `<p>first component works!</p>`,
      providers: [DestroyService],
    })
    export class FirstComponent implements OnInit {
      ...
    
      constructor(
        private messageService: MessageService,
        private readonly destroy$: DestroyService
      ) {}
    
      ngOnInit(): void {
        ...
      }
    }
    
    우리는 이전과 완전히 같은 결과를 얻었다.우리는 필요한 모든 구성 요소에서 takeUntil의 실례를 제공할 수 있다.

    결론


    최종적으로 RxJS 구독을 관리하는 더 좋은 방법은 각도 서비스.unsubscribe()를 통해 사용하는 것이라고 생각합니다.몇 가지 이점은 다음과 같습니다.
  • 코드 감소
  • 우리가 흐름
  • 을 죽일 때 사건 완성을 촉발
  • 구현
  • 중 잊어버리기.next() 또는 .complete(), ngOnDestroy() 방법의 기회 감소
    GitHub repo는 예제here를 제공합니다.

    좋은 웹페이지 즐겨찾기