Не отписался — без памяти остался

от автора

Предисловие

Речь пойдёт про мощный инструмент, реализующий собой подход реактивного программирования и в разы упрощающий разработку — RxJS. В частности разберём один момент, про который нужно не забывать при использовании этой библиотеки, а именно — отписки.

Да, да, этот базовый момент может упускаться разработчиком, и это в свою очередь может привести к утечке памяти — об этом далее.

Кейс — отправка запросов на бэк и показ данных в реальном времени

Задача

Представим, что мы разрабатываем приложение, которое в реальном времени показывает нам курсы валют. Клиент часто обращается к бэку либо через обычные запросы, либо через веб-сокеты, и нам нужно каждый ответ от бэка отображать на стороне клиента.

Решение

Будем использовать RxJS в связке с Angular и нашу функцию, которая будет отдавать нам рандомный курс.

Функция будет нам отдавать через определённый промежуток времени курс, который будет сгенерирован рандомайзером:

// fake-currency.function.ts  import { interval } from 'rxjs'; import { map } from 'rxjs/operators';  // Простой рандомайзер, который отдаёт случайное число в определённом диапазоне function getRandomByLimits(min: number, max: number) {   return Math.round(Math.random() * (max - min) + min); }  // Функция, отдающая фейковый курс export function fakeCurrency(time: number) {   const minLimit = 65;   const maxLimit = 78;   return interval(time).pipe(map(() => getRandomByLimits(minLimit, maxLimit))); }

Рандомные курсы валют у нас есть, теперь нам нужно реализовать компонент нашей страницы.

// my-component.component.ts  import { Component } from '@angular/core'; import { fakeCurrency } from '../fake-currency.function';  @Component({   selector: 'my-component',   templateUrl: './my-component.component.html',   styleUrls: ['./my-component.component.css'], }) export class MemoryLeakComponent {   // свойство, которое мы будем использовать в шаблоне   num: number;    constructor() {     fakeCurrency(500).subscribe((num) => {       this.num = num;     });   } }

Шаблон:

<!-- my-component.component.html --> <p>Random exchange rate: {{ num }}</p>

Поздравляю!!! ???
У нас получилось вывести рандомный курс валюты на страницу!!!

Также поздравляю с получением первой утечки памяти!)

О какой утечке памяти идёт речь?

Дело в том, что если мы с этой страницы переместимся на другую, то работа функции fakeCurrency не остановится. Мы можем это проверить добавив логирование в подписку.

// my-component.component.ts  import { Component } from '@angular/core'; import { fakeCurrency } from '../fake-currency.function';  @Component({   selector: 'my-component',   templateUrl: './my-component.component.html',   styleUrls: ['./my-component.component.css'], }) export class MemoryLeakComponent {   // свойство, которое мы будем использовать в шаблоне   num: number;    constructor() {     // Случайный id     const id = Math.floor(Math.random() * 100000);     fakeCurrency(500).subscribe((num) => {       console.log(`ID: ${id}`, num);       this.num = num;     });   } }

Как мы видим — отображения курса на странице нет, но в консоль продолжают сыпаться логи. Если юзер продолжительное время будет переключаться между страницами, то такая утечка может вызвать дикие тормоза на его компьютере.

Как же нам избежать этого?

Конечно же отписаться!)

И способов отписки существует несколько.

Async Pipe

В данном случае этот способ наиболее предпочтительный. Немножко изменим наш код:

// my-component.component.ts  import { Component } from '@angular/core'; import { fakeCurrency } from '../fake-currency.function'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators';  @Component({   selector: 'my-component',   templateUrl: './my-component.component.html',   styleUrls: ['./my-component.component.css'], }) export class MemoryLeakComponent {   // свойство, которое мы будем использовать в шаблоне   num$: Observable<number>;    constructor() {     // Случайный id     const id = Math.floor(Math.random() * 100000);     this.num$ = fakeCurrency(500)       .pipe(         tap((num) => console.log(`ID: ${id}`, num))       );   } }

Не забываем про наш шаблон:

<!-- my-component.component.html --> <p>Random exchange rate: {{ num$ | async }}</p>

Destroy Subject Pattern

Если же нам значение в шаблоне не нужно, но при этом подписаться всё-таки нужно, то можно воспользоваться паттерном Destroy Subject и оператором отписки takeUntil:

// my-component.component.ts  import { Component } from '@angular/core'; import { fakeCurrency } from '../fake-currency.function'; import { takeUntil } from 'rxjs/operators';  @Component({   selector: 'my-component',   templateUrl: './my-component.component.html',   styleUrls: ['./my-component.component.css'], }) export class MemoryLeakComponent implements OnDestroy {   // свойство, которое мы будем использовать в шаблоне   num: number;   destroy$ = new Subject();    constructor() {     // Случайный id     const id = Math.floor(Math.random() * 100000);     fakeCurrency(500)       // Прокидываем оператор в поток и передаём ему другой поток       .pipe(takeUntil(this.destroy$))       .subscribe((num) => {         console.log(`ID: ${id}`, num);         this.num = num;       });   }      ngOnDestroy() {     // завершаем поток     // когда переданный поток завершается, то оператор takeUntil отписывается от текущего потока     this.destroy$.next();     this.destroy$.complete();   } }

Может есть решение красивее?

Конечно, есть!)

Не забываем, что Subject сущности — те же классы, от которых мы можем наследоваться. (Нагло беру пример из библиотеки taiga-ui)

Создаём сервис, который будет наследоваться от ReplaySubject и имплементировать OnDestroy интерфейс:

// destroy.service.ts import { Injectable, OnDestroy } from '@angular/core'; import { ReplaySubject } from 'rxjs';  @Injectable() export class DestroyService extends ReplaySubject<void> implements OnDestroy {   constructor() {     super();   }    ngOnDestroy() {     this.next();     this.complete();   } }

Именно в компонент нужно обязательно запровайдить DestroyService, с провайдингом в модуль такое не прокатит:

// my-component.component.ts  import { Component, Inject } from '@angular/core'; import { fakeCurrency } from '../fake-currency.function'; import { takeUntil } from 'rxjs/operators'; import { DestroyService } from '../destroy.service';  @Component({   selector: 'my-component',   templateUrl: './my-component.component.html',   styleUrls: ['./my-component.component.css'],   // Провайдим сервис   providers: [DestroyService] }) export class MemoryLeakComponent implements OnDestroy {   // свойство, которое мы будем использовать в шаблоне   num: number;    constructor(@Inject(DestroyService) private destroy$: Observable<void>) {     // Случайный id     const id = Math.floor(Math.random() * 100000);     fakeCurrency(500)       // Прокидываем оператор в поток и передаём ему параметром другой поток       .pipe(takeUntil(destroy$))       .subscribe((num) => {         console.log(`ID: ${id}`, num);         this.num = num;       });   } }

Подытожим:

  1. Почти во всех случаях нам нужно отписываться (исключение: потоки, которые завершаются самостоятельно);

  2. По возможности больше использовать async pipe, если данные отображаются в шаблоне;

  3. Если не обойтись без подписки в компоненте, то используйте операторы take*;

На этом всё, весь код из статьи можно найти тут.


ссылка на оригинал статьи https://habr.com/ru/post/667504/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *