[Перевод] Отложенное применение функционала директив в Angular

Недавно мне надо было решить задачу по смене старого механизма для вывода всплывающих подсказок, реализованного средствами нашей библиотеки компонентов, на новый. Я, как всегда, решил не заниматься изобретением велосипеда. Для того чтобы приступить к решению этой задачи, я занялся поисками опенсорсной библиотеки, написанной на чистом JavaScript, которую можно было бы поместить в директиву Angular и в таком виде использовать.

eswdkavwzb12runf9unmttdcsf0.jpeg

В моём случае, так как я много работаю с popper.js, я нашёл библиотеку tippy.js, написанную тем же разработчиком. Для меня такая библиотека выглядела как идеальное решение задачи. Библиотека tippy.js обладает обширным набором возможностей. С её помощью можно создавать и всплывающие подсказки (элементы tooltip), и многие другие элементы. Эти элементы можно настраивать с помощью тем, они быстры, строго типизированы, обеспечивают доступность контента и отличаются многими другими полезными возможностями.
Я начал работу с создания директивы-обёртки для tippy.js:

@Directive({ selector: '[tooltip]' })
export class TooltipDirective {
  private instance: Instance;
  private _content: string;

  get content() {
    return this._content;
  }

  @Input('tooltip') set content(content: string) {
    this._content = content;
    if (this.instance) this.instance.setContent(content);
  }

  constructor(private host: ElementRef, private zone: NgZone) {}

  ngAfterViewInit() {
    this.zone.runOutsideAngular(() => {
      this.instance = tippy(this.host.nativeElement, {
        content: this.content,
      });
    });
}


Всплывающую подсказку создают, вызывая функцию tippy и передавая ей элементы host и content. Кроме того, мы вызываем tippy за пределами Angular Zone, так как нам не нужно, чтобы события, регистрируемые tippy, приводили бы к запуску цикла обнаружения изменений.

Теперь воспользуемся всплывающей подсказкой в большом списке из 700 элементов:

@Component({
  selector: 'my-app',
  template: `
    
          
  •          {{ item.label }}       
  •     
  ` }) export class AppComponent {   data = Array.from({ length: 700 }, (_, i) => ({     id: i,     label: `Value ${i}`,   })); }


Всё работает так, как ожидается. Каждый элемент выводит всплывающую подсказку. Но мы можем решить эту задачу лучше. В нашем случае создано 700 экземпляров tippy. А для каждого элемента средствами tippy.js было добавлено 4 прослушивателя событий. Это означает, что мы зарегистрировали 2800 прослушивателей (700×4).

Для того чтобы увидеть это своими глазами, можно воспользоваться методом getEventListeners в консоли инструментов разработчика Chrome. Конструкция вида getEventListeners(element) возвращает сведения о прослушивателях событий, зарегистрированных для заданного элемента.

c85ea95b6a8f60bc535c212f5cce21cd.png


Сводные данные обо всех прослушивателях событий

Если оставить код в таком виде, это может подействовать на потребление памяти приложением и на время его первого рендеринга. Особенно это касается вывода страницы на мобильных устройствах. Поразмыслим над этим. Нужно ли создавать экземпляры tippy для элементов, которые не выводятся в области просмотра? Нет, не нужно.

Воспользуемся API IntersectionObserver для того чтобы отложить включение поддержки всплывающих подсказок до момента появления элемента на экране. Если вы не знакомы с API IntersectionObserver — взгляните на документацию. 

Создадим для IntersectionObserver обёртку, представленную наблюдаемым объектом:

const hasSupport = 'IntersectionObserver' in window;

export function inView(
  element: Element,
  options: IntersectionObserverInit = {
    root: null,
    threshold: 0.5,
  }
) {
  return new Observable((subscriber) => {
    if (!hasSupport) {
      subscriber.next(true);
      subscriber.complete();
    }

    const observer = new IntersectionObserver(([entry]) => {
      subscriber.next(entry.isIntersecting);
    }, options);

    observer.observe(element);

    return () => observer.disconnect();
  });
}


Мы создали наблюдаемый объект, который сообщает подписчикам о моменте пересечения элемента с заданной областью. Кроме того, тут мы проверяем поддержку IntersectionObserver браузером. Если браузер не поддерживает IntersectionObserver — мы просто выдаём true и завершаем работу. Пользователи IE сами виноваты в своих страданиях.

Теперь наблюдаемый объект inView мы можем использовать в директиве, реализующей функционал всплывающей подсказки:

@Directive({ selector: '[tooltip]' })
export class TooltipDirective {
  ...

  ngAfterViewInit() {
    // Не забудьте отписаться
    inView(this.host.nativeElement).subscribe((inView) => {
      if (inView && !this.instance) {
        this.zone.runOutsideAngular(() => {
          this.instance = tippy(this.host.nativeElement, {
            content: this.content,
          });
        });
      } else if (this.instance) {
        this.instance.destroy();
        this.instance = null;
      }
    });
  }
}


Снова запустим код для анализа количества прослушивателей событий.

14eb0dbc72735f1086763e4fcc4527e2.png


Сводные данные обо всех прослушивателях событий после доработки проекта

Отлично. Теперь мы создаём всплывающие подсказки только для видимых элементов.

Поищем ответы на пару вопросов, связанных с новым решением.

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

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

Итоги


Здесь мы поговорили о том, как откладывать применение функционала директив. Это позволяет приложению быстрее загружаться и потреблять меньше памяти. Пример со всплывающей подсказкой — это лишь один из многих случаев, в которых может применяться подобная техника. Уверен, вы найдёте немало способов её использования в собственных проектах.

А как вы решили бы задачу по выводу большого списка элементов, каждый из которых нужно оснастить всплывающей подсказкой?

Напоминаем, что у нас продолжается конкурс прогнозов, в котором можно выиграть новенький iPhone. Еще есть время ворваться в него, и сделать максимально точный прогноз по злободневным величинам.


a_bsaactpbr8fltzymtkhqbw1d4.png

© Habrahabr.ru