DevGang
Авторизоваться

Основы Angular: Пошаговое понимание асинхронного канала

Асинхронный канал может иметь огромное значение в вашей стратегии обнаружения изменений для вашего приложения Angular. Если до сих пор это сбивало вас с толку, ознакомьтесь с этим пошаговым объяснением.  Разберемся вместе.

В Angular асинхронный канал - это канал, который, по сути, выполняет следующие три задачи:

  • Он подписывается на наблюдаемое или обещание и возвращает последнее сгенерированное значение;
  • Всякий раз, когда выдается новое значение, он помечает компонент для проверки. Что означает, что Angular запустит детектор изменений (Change Detector) для этого компонента в следующем цикле.
  • Он отписывается от наблюдаемого, когда компонент уничтожается.

Кроме того, в качестве наилучшей практики рекомендуется попробовать использовать компонент стратегии обнаружения изменений в onPush с асинхронным каналом для подписки на наблюдаемые объекты.

Если вы новичок в Angular, возможно приведенное выше объяснение асинхронного канала покажется вам ошеломляющим. Итак, в этой статье мы попытаемся шаг за шагом понять асинхронный канал, используя примеры кода. Просто создайте новый проект Angular и следуйте инструкциям; в конце поста у вас должно быть практическое представление об асинхронном канале.

Создание Service

Давайте начнем с создания интерфейса Product и сервиса.

export interface IProduct {

     Id : string; 
     Title : string; 
     Price : number; 
     inStock : boolean;

}

После создания интерфейса IProduct создайте массив IProduct внутри службы Angular для выполнения операций чтения и записи

import { Injectable } from '@angular/core';
import { IProduct } from './product.entity';

@Injectable({
  providedIn: 'root'
})
export class AppService {

  Products : IProduct[] = [
    {
      Id:"1",
      Title:"Pen",
      Price: 100,
      inStock: true 
    },
    {
      Id:"2",
      Title:"Pencil",
      Price: 200,
      inStock: false 
    },
    {
      Id:"3",
      Title:"Book",
      Price: 500,
      inStock: true 
    }
  ]

  constructor() { }
}

Помните, что в реальных приложениях вы получаете данные из API; однако, здесь мы имитируем операции чтения и записи в локальном массиве, чтобы сосредоточиться на асинхронном канале. 

Чтобы выполнять операции чтения и записи, давайте обернем массив Products внутри BehaviorSubject и создадим новый массив каждый раз, когда новый элемент помещается в массив Products

Для этого добавьте код в сервис, как указано ниже: 

  Products$ : BehaviorSubject<IProduct[]>; 
  constructor() {
    this.Products$ = new BehaviorSubject<IProduct[]>(this.Products);
   }
  
   AddProduct(p: IProduct): void{
    this.Products.push(p);
    this.Products$.next(this.Products);
   }

Давайте пройдемся по коду:

  • BehaviorSubject — это тип Subject, который выдает значение по умолчанию или последнее переданное значение. Мы используем BehaviorSubject для первоначальной генерации массива Products по умолчанию.
  • В методе AddProduct мы передаем продукт и помещаем его в массив.
  • В методе AddProduct, после помещения элемента в массив Products, мы испускаем обновленный массив Products.

На данный момент сервис готов. Далее мы создадим два компонента - один для добавления продукта и один для отображения всех продуктов в таблице.

Добавление продукта

Создайте компонент с именем AddProduct и добавьте реактивную форму для приема информации о продукте. 

  productForm: FormGroup;
  constructor(private fb: FormBuilder, private appService: AppService) {
    this.productForm = this.fb.group({
      Id: ["", Validators.required],
      Title: ["", Validators.required],
      Price: [],
      inStock: []
    })
  }

Мы используем сервис FormBuilder для создания FormGroup и шаблона компонента, используя productForm с HTML-формой как показано ниже:

<form (ngSubmit)='addProduct()' [formGroup]='productForm'>
    <input formControlName='Id' type="text" class="form-control" placeholder="Enter ID" />
    <input formControlName='Title' type="text" class="form-control" placeholder="Enter Title" />
    <input formControlName='Price' type="text" class="form-control" placeholder="Enter Price" />
    <input formControlName='inStock' type="text" class="form-control" placeholder="Enter Stock " />
    <button [disabled]='productForm.invalid' class="btn btn-default">Add Product</button>
</form>

И в функции AddProduct мы проверим, является ли форма действительной. Если да, мы вызываем службу, чтобы поместить один продукт в массив Products. Функция AddProduct должна выглядеть следующим образом:

  addProduct() {
    if (this.productForm.valid) {
      this.appService.AddProduct(this.productForm.value);
    }
  }

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

Перечисление товаров

После добавления компонента в список товаров выполните обычные шаги:

  1. Установите стратегию Change Detection компонента Default;
  2. Внедрите AppService в компонент;
  3. Используйте метод subscribe подписки для получения данных из наблюдаемого объекта.
@Component({
  selector: 'app-list-products',
  templateUrl: './list-products.component.html',
  styleUrls: ['./list-products.component.css'],
  changeDetection: ChangeDetectionStrategy.Default
})
export class ListProductsComponent implements OnInit, OnDestroy {

  products: IProduct[] = []
  productSubscription?: Subscription
  constructor(private appService: AppService) { }

  productObserver = {
    next: (data: IProduct[]) => { this.products = data; },
    error: (error: any) => { console.log(error) },
    complete: () => { console.log('product stream completed ') }
  }
  ngOnInit(): void {
    this.productSubscription = this.appService.Products$.subscribe(this.productObserver)
  }

  ngOnDestroy(): void {
    if (this.productSubscription) {
      this.productSubscription.unsubscribe();
    }
  }
}

Давайте пройдемся по коду:

  • Переменная products содержит массив, возвращаемый сервисом;
  • productSubscription - переменная типа подписки RxJS для назначения подписки, возвращаемой из метода подписки наблюдаемого объекта;
  • productObserver - это объект с функциями следующего, ошибочного и полного обратного вызова;
  • Наблюдатель productObserver передается методу подписки;
  • В ngOnDestrory() пути cyclehook мы отказываемся от подписки на наблюдаемый объект.

В шаблоне вы можете отобразить продукты в виде таблицы, как показано ниже:

<table>
    <thead>
        <tr>
            <th>Id</th>
            <th>Title</th>
            <th>Price</th>
            <th>inStock</th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let p of products">
            <td>{{p.Id}}</td>
            <td>{{p.Title}}</td>
            <td>{{p.Price}}</td>
            <td>{{p.inStock}}</td>
        </tr>
    </tbody>
</table>

Использование компонентов

Мы будем использовать два этих компонента, как родственные компоненты, как показано ниже:

<h1>{{title}}</h1>

<app-add-product></app-add-product>

<hr/>
<app-list-products></app-list-products>

Важным моментом, на который следует обратить внимание, является то, что компонент AddProduct и компонент ListProducts не связаны между собой. Есть только два способа, которыми они могут передавать данные друг другу:

  1. Путем обмена данными через родительский компонент;
  2. При общении с помощью сервиса.

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

Запуск приложения

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

Как вы заметили, вы можете добавить этот продукт, нажав кнопку «Add Product». Это вызывает функцию в службе, которая обновляет массив и выдает обновленный массив из наблюдаемого.

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

Использование стратегии onPush Change Detection

Если вы помните, для компонента ListProducts стратегии Change Detection установлено значение default. Теперь давайте продолжим и изменим стратегию на onPush:

И снова запустите приложение. Как вы правильно заметили, когда вы добавляете товар из компонента AddProduct, он добавляется в массив, и даже обновленный массив выдается из сервиса. Тем не менее компонент ListProducts не обновляется. Это происходит потому, что для стратегии change detection компонента ListProducts установлено значение onPush.

Изменение стратегии Change Detection на onPush предотвращает обновление таблицы новыми товарами.

Для компонента со стратегией onPush change detection Angular запускает детектор изменений только тогда, когда компоненту передается новая ссылка. Однако, когда наблюдаемая испускает новый элемент, она не дает новую ссылку. Следовательно, Angular не запускает детектор изменений, и обновленный массив продуктов не проецируется в компоненте.

Как мы можем это исправить?

Мы можем исправить это вручную, вызвав Change Detector. Для этого внедрите ChangeDetectorRef в компонент и вызовите метод markForCheck()

export class ListProductsComponent implements OnInit, OnDestroy {

  products: IProduct[] = []
  productSubscription?: Subscription
  constructor(private appService: AppService, 
    private cd: ChangeDetectorRef) {

   }

  productObserver = {
    next: (data: IProduct[]) => {
       this.products = data; 
      this.cd.markForCheck(); 
    },
    error: (error: any) => { console.log(error) },
    complete: () => { console.log('product stream completed ') }
  }
  ngOnInit(): void {
    this.productSubscription = this.appService.Products$.subscribe(this.productObserver)
  }

  ngOnDestroy(): void {
    if (this.productSubscription) {
      this.productSubscription.unsubscribe();
    }
  }
}

Выше мы выполнили следующие задачи:

  1. Мы внедрили Angular ChangeDetectorRef в компонент;
  2. Метод markForCheck() помечает этот компонент и все его родительские компоненты как нечистые, чтобы Angular проверял изменения в следующем цикле Change Detection.

Теперь при запуске приложения вы сможете обновленный массив продуктов.

Анализ Subscribe Approach

Как вы уже видели, в компоненте, установленном на onPush, для работы с наблюдаемыми вы выполняете следующие шаги.

  1. Подпишитесь на наблюдаемое.
  2. Запустите вручную Change Detection.
  3. Отпишитесь от наблюдаемого.

Преимущества подхода subscribe():

  • Свойство может использоваться в нескольких местах шаблона.
  • Свойство может использоваться в различных местах класса компонента.
  • Вы можете запускать настраиваемую бизнес-логику при подписке на наблюдаемый объект.

Некоторые из недостатков:

  • Для стратегии onPush change detection необходимо вручную пометить компонент для запуска change detector с помощью метода markForCheck.
  • Вы должны однозначно отказаться от подписки на наблюдаемые объекты.

Этот подход может выйти из под контроля, когда в компоненте используется много наблюдаемых объектов. Если мы пропустим отписку от какой-либо наблюдаемой, это может привести к утечке памяти и т.д.

Вышеуказанные проблемы могут быть решены с помощью async pipe.

The Async Pipe

Async pipe - это лучший и более рекомендуемый способ работы с наблюдаемыми в компоненте. Async pipe выполняет следующие три задачи:

  1. Он подписывается на наблюдаемое и выдает последнее выданное значение.
  2. Когда выдается новое значение, оно помечает компонент, подлежащее проверке на наличие изменений.
  3. Async pipe автоматически отменяет подписку при уничтожении компонента, чтобы избежать потенциальных утечек памяти.

Таким образом, async pipe выполняет все три задачи, которые вы выполняли вручную для подхода с подпиской. 

Давайте изменим компонент ListProducts, чтобы использовать async pipe.

@Component({
  selector: 'app-list-products',
  templateUrl: './list-products.component.html',
  styleUrls: ['./list-products.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListProductsComponent implements OnInit {

  products?: Observable<IProduct[]>;
  constructor(private appService: AppService) {}
  ngOnInit(): void {
    this.products = this.appService.Products$;
  }
}

Мы удалили весь код и присвоили наблюдаемую отдачу от сервиса переменной products. В шаблоне для отображения данных теперь используйте async pipe.

<table>
    <thead>
        <tr>
            <th>Id</th>
            <th>Title</th>
            <th>Price</th>
            <th>inStock</th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let p of products | async">
            <td>{{p.Id}}</td>
            <td>{{p.Title}}</td>
            <td>{{p.Price}}</td>
            <td>{{p.inStock}}</td>
        </tr>
    </tbody>
</table>

Использование async pipe делает код чище: и вам не нужно вручную запускать детектор изменений для стратегии onPush Change Detection. В приложении вы видите, что компонент ListProducts повторно отображается всякий раз, когда добавляется новый товар.

Всегда рекомендуется и наилучшая практика заключается в том, чтобы:

  1. Сохраняйте стратегию change detection компонентов в режиме onPush
  2. Используйте async pipe для работы с наблюдаемыми объектами

Надеюсь, данная статья окажется для вас полезной и вы готовы использовать async pipe в ваших проектах.

#Angular
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу