Оптимизация работы с браузерными событиями в Ангуляр-директиве
Если у вас директива слушает браузерные события, например движения/клики мыши или клавиатуры через 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 не будет реагировать, потому нужно применять этот способ оптимизации по потребности.