Оптимизация работы с браузерными событиями в Ангуляр-директиве

Если у вас директива слушает браузерные события, например движения/клики мыши или клавиатуры через HostListeener, то это может быть проблемой с перформансом.

Пример:

Допустим у вас директива, которая слушает в элементе с атрибутом editablecontent=true и в output отдает начало и конец текстового выделения.

Решение:


import {Directive, ElementRef, EventEmitter, HostListener, inject, Output,} from '@angular/core';

interface SelectedEvent {
  start: number;
  end: number;
}

@Directive({
  standalone: true,
  selector: '[selectedOffset]',
})
export class SelectChildren {

  @Output() onSelect = new EventEmitter();

  private readonly elementRef = inject(ElementRef);

  @HostListener('mouseup')
  @HostListener('mousedown')
  @HostListener('mousemove')
  @HostListener('keyup', ['$event'])
  listenEvents(event: KeyboardEvent) {
    this.reportSelection();
  }

  private reportSelection(): void {
    this.onSelect.next(this.getSelectionCharacterOffsetWithin(this.elementRef.nativeElement));
  }

  private getSelectionCharacterOffsetWithin(element: HTMLElement): SelectedEvent | null {
    const doc = element.ownerDocument;
    const win = doc.defaultView;

    const sel = win?.getSelection();

    if (!!sel && sel.rangeCount > 0) {
      const range = sel.getRangeAt(0);
      const preCaretRange = range.cloneRange();
      preCaretRange.selectNodeContents(element);
      preCaretRange.setEnd(range.startContainer, range.startOffset);
      const start = preCaretRange.toString().length;
      preCaretRange.setEnd(range.endContainer, range.endOffset);
      const end = preCaretRange.toString().length;

      return { start: start, end: end };
    }

    return null;
  }
}


В данном примере очень агрессивная политика у слушателя @HostListener(’mousemove’), который может очень сильно повлиять на перформанс работы вашего приложения.

Оптимизация:

Можно конечно слушать события mousemove только если произошло событие mousedown, сделав флаг в приватной переменной, а на mouseup его не обрабатывать, но есть споособ написать его через RxJs, который даст дополнительные возможности. Можем подписаться на mousemove только после события mousedown и отписываться на эмите mouseup событий.


  private mousedown$ = fromEvent(this.elementRef.nativeElement, 'mousedown');
  private mouseup$ = fromEvent(this.elementRef.nativeElement, 'mouseup');
  private mousemove$ = fromEvent(this.elementRef.nativeElement, 'mousemove');

  constructor() {
    this.mousedown$
      .pipe(
        switchMap(() => this.mousemove$),
        takeUntil(this.mouseup$),
        repeat(),
      )
      .pipe(
        takeUntilDestroyed(),
      )
      .subscribe(e => this.reportSelection())
  }


Дополнительной точкой оптимизации может быть выполнение логики снаружи от зоны, чтобы наш UI не тригерился на этой логике:


  private readonly zone = inject(NgZone);

  constructor() {
    this.zone.runOutsideAngular(() => {
      this.mousedown$
        .pipe(
          switchMap(() => this.mousemove$),
          takeUntil(this.mouseup$),
          repeat(),
        )
        .pipe(
          takeUntilDestroyed(),
        )
        .subscribe(e => this.reportSelection())
    })
  }


Но такого рода оптимизация повлияет на UI, если вам нужно реактивно отрисовывать изменения на mousemove — то UI не будет реагировать, потому нужно применять этот способ оптимизации по потребности.

15 июля   angular
Популярное