Оптимизация обнаружения изменений в Angular, вызванных событиями DOM

Jul 21, 2019

Как вы, возможно, уже знаете Angular использует ZoneJS, чтобы выяснить, когда асинхронная задача завершена и как результат, должен быть запущен механизма обнаружения изменений (change detection).

AddEventListener - это одно из API браузера, исправленное в ZoneJS. Так, например, когда мы регистрируем событие в нашем компоненте:

import { fromEvent } from 'rxjs';

@Component({
    selector: 'component',
})
export class TestComponent {

    ngOnInit() {
        fromEvent(window, 'scroll').subscribe(...);
    }

    // Or
    @HostListener('window:scroll')
    onScroll() {}
}

Каждый раз, когда происходит событие, NgZone уведомляет Angular, что приводит к запуску нового цикла обнаружения изменений (change detection). Мы можем легко увидеть это в действии, добавив геттер в шаблон нашего компонента.

View будет проверено, если только ваша стратегия обнаружения изменений (change detection) компонентов не установлена на OnPush или Detach.

Метод detach отменяет проверки на текущем view. (Все, что вам нужно знать об обнаружении изменений в Angular / habr)

Давайте рассмотрим, что происходит под капотом:

Angular подписан на observable onMicrotaskEmpty, который производит эмит, когда все задачи выполнены и зона стабильна. Когда этот observable эмитит, Angular вызывает метод tick(), который запускает механизм обнаружения изменений в каждом представлении.

Теперь представьте, как неэффективно будет запускать новый цикл обнаружения изменений каждый раз, когда запускается частое событие, например, scroll (которое может легко достигать 50 событий в секунду).

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

Давайте рассмотрим три способа оптимизации этого поведения:

Отключаем глобально

Наш первый вариант - добавить глобальный флаг, который позволяет нам указывать ZoneJS, какие события не нужно патчить.

Создайте новый файл с именем zone-flags.ts и добавьте следующую строку:

window.__zone_symbol__UNPATCHED_EVENTS = ['scroll'];

Затем импортируйте его в файл polyfills.ts:

import './zone-flags.ts';
import 'zone.js/dist/zone';

Теперь вы увидите, что обнаружение изменений больше не запускается, когда запускается событие прокрутки. Используйте этот параметр с осторожностью, так как от него могут зависеть неоптимизированные сторонние библиотеки. Проверьте файл polyfills.ts на наличие дополнительных параметров зоны, которые вы можете применить.

Запуск за пределами зоны

Второй вариант - запустить событие за пределами Angular зоны. Мы можем сделать это, получив ссылку на NgZone через DI и вызвав событие внутри функции обратного вызова runOutsideAngular:

export class TestComponent {
  constructor(private zone: NgZone) {}

  ngOnInit() {
    this.zone.runOutsideAngular(() => {
    // don't forget to unsubscribe
    fromEvent(window, 'scroll').subscribe(...);
    });
  }
  // ...
}

Запуск функций через runOutsideAngular позволяет нам исключить зону Angular и выполнять задачи, которые не вызывают обнаружение изменений Angular.

Мы можем пойти дальше. В настоящее время наш код не является чистым, пригодным для повторного использования или дружественным RxJS. Давайте создадим пользовательский оператор для решения этих проблем:

export function outsideZone<T>(zone: NgZone) {
    return function(source: Observable<T>) {
      return new Observable(observer => {
        let sub: Subscription;
        zone.runOutsideAngular(() => {
          sub = source.subscribe(observer);
        });

        return sub;
      });
  };
}

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

В нашем случае мы берем ссылку на NgZone и вызываем runOutsieAngular, передавая ему observable, который мы хотим запустить за пределами Angular. Мы также возвращаем оригинальную подписку, чтобы наши пользователи могли отписаться от нее. Теперь мы можем использовать его везде, где нам нужно:

export class TestComponent {
    constructor(private zone: NgZone) {}

  ngOnInit() {
    fromEvent(window, 'scroll')
      .pipe(outsideZone(this.zone))
      .subscribe(...);
  }
}

Если мы используем Ivy, мы можем пропустить ту часть, где мы передаем ссылку на ngZone, и получить ссылку на нее с помощью функции ɵɵdirectiveInject:

import { ɵɵdirectiveInject as directiveInject} from '@angular/core';

export function outsideZone<T>() {
  return function(source: Observable<T>) {
    return new Observable(observer => {
      let sub: Subscription;
      ɵɵdirectiveInject(NgZone).runOutsideAngular(() => {
        sub = source.subscribe(observer);
      });

      return sub;
    });
  };
}
fromEvent(window, 'scroll').pipe(outsideZone()).subscribe(...);

источник

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