Предисловие
Речь пойдёт про мощный инструмент, реализующий собой подход реактивного программирования и в разы упрощающий разработку — 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; }); } }
Подытожим:
-
Почти во всех случаях нам нужно отписываться (исключение: потоки, которые завершаются самостоятельно);
-
По возможности больше использовать async pipe, если данные отображаются в шаблоне;
-
Если не обойтись без подписки в компоненте, то используйте операторы
take*;
На этом всё, весь код из статьи можно найти тут.
ссылка на оригинал статьи https://habr.com/ru/post/667504/
Добавить комментарий