Оптимальный повторный вход в ngzone из события eventemitter

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

@Component({ ... })
export class TestComponent {

  @Output()
  emitter = new EventEmitter<void>();

  constructor(private ngZone: NgZone) {}

  ngOnInit() {
    this.ngZone.runOutsideAngular(() => {
        // ...
    });    
  }

}

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

@Component({ ... })
export class TestComponent {

  @Output()
  emitter = new EventEmitter<void>();

  private lib: Lib;

  constructor(private ngZone: NgZone) {}

  ngOnInit() {
    this.ngZone.runOutsideAngular(() => {
      this.lib = new Lib();
    });

    this.lib.on('click', () => {
      this.emitter.emit();
    });
  }

}

Проблема в том, что этот эмиттер не запускает обнаружение изменений, потому что он запускается за пределами зоны. Что тогда возможно, так это повторно войти в зону:

@Component({ ... })
export class TestComponent {

  @Output()
  emitter = new EventEmitter<void>();

  private lib: Lib;

  constructor(private ngZone: NgZone) {}

  ngOnInit() {
    this.ngZone.runOutsideAngular(() => {
      this.lib = new Lib();
    });

    this.lib.on('click', () => {
      this.ngZone.run(() => this.emitter.emit());
    });
  }

}

Наконец, я подхожу к вопросу. Этот this.ngZone.run форсирует обнаружение изменений, даже если я не прослушивал это событие в родительском компоненте:

<test-component></test-component>

который не нужен, потому что, ну, я не подписывался на это событие => нечего обнаруживать.

Какое решение этой проблемы?

Для тех, кому интересен реальный пример, источник вопроса - здесь.

4
0
1 525
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Имейте в виду, что привязка @Output(), которая генерирует значение, по определению является триггером для обнаружения изменений в родительском элементе. Хотя для этой привязки может не быть никаких слушателей, в родительском шаблоне может быть логика, которая ссылается на компонент. Может быть, через exportAs или запрос @ViewChild. Итак, если вы испускаете значение, вы сообщаете родителю, что состояние компонента изменилось. Возможно, в будущем команда Angular изменит это, но сейчас это работает.

Если вы хотите обойти обнаружение изменений для этого наблюдаемого, не используйте декоратор @Output. Удалите декоратор и получите доступ к свойству emtter через exportAs или используйте @ViewChild в родительском компоненте.

Посмотрите, как работают реактивные формы. Директивы для элементов управления имеют общедоступные наблюдаемые для изменений, которые не используют @Output. Это просто общедоступные наблюдаемые объекты, и вы можете подписаться на них.

Поэтому, если вы хотите иметь наблюдаемую, которая не связана с обнаружением изменений, просто сделайте ее общедоступной. Так все просто. Добавление логики для выдачи только при наличии подписчика на @Output затрудняет понимание компонента, когда вы позже читаете исходный код.

С учетом сказанного, вот как я отвечу на ваш вопрос, чтобы вы могли использовать @Output() только при наличии подписчика.

@Component({})
export class TestComponent implements OnInit {

    private lib: Lib;

    constructor(private ngZone: NgZone) {
    }

    @Output()
    public get emitter(): Observable<void> {
        return new Observable((subscriber) => {
            this.initLib();
            this.lib.on('click', () => {
                this.ngZone.run(() => {
                    subscriber.next();
                });
            });
        });
    }

    ngOnInit() {
        this.initLib();
    }

    private initLib() {
        if (!this.lib) {
            this.ngZone.runOutsideAngular(() => {
                this.lib = new Lib();
            });
        }
    }
}

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

Это интересно. Совсем забыл про подделку EventEmitter :) Мне нравится ваше решение. На самом деле это не так уж и плохо, если вы создадите фабрику с понятным именем, например. @Output() emitter = LazyEventEmitter(/* all required properties for creating observable */), тогда становится вполне понятно.

smnbbrv 10.08.2018 17:04

@cgTag Спасибо за ответ, это золото! > smnbbrv У вас есть пример кода для идеи декоратора LazyEventEmitter? Мне нравится подход.

bertrandg 19.11.2018 12:52

@bertrandg извините за поздний комментарий, ваш комментарий не вызвал уведомления, поэтому я просто случайно нашел его. Похоже, этого ответа было недостаточно, чтобы объяснить суть дела, и на самом деле, на мой взгляд, это не окончательный / лучший вариант. Итак, я подготовил еще один ответ, в котором подробно объясняется, как использовать лучший подход, чем предлагает этот исходный ответ. Это дает вам возможность получить.

smnbbrv 10.12.2018 11:52
Ответ принят как подходящий

Прежде всего, благодаря ответу cgTag. Это привело меня в лучшее направление, которое более читабельно, удобно в использовании и вместо геттера использует наблюдаемую естественную лень.

Вот хорошо объясненный пример:

export class Component {

  private lib: any;

  @Output() event1 = this.createLazyEvent('event1');

  @Output() event2 = this.createLazyEvent<{ eventData: string; }>('event2');

  constructor(private el: ElementRef, private ngZone: NgZone) { }

  // creates an event emitter that binds to the library event
  // only when somebody explicitly calls for it: `<my-component (event1)="..."></my-component>`
  private createLazyEvent<T>(eventName: string): EventEmitter<T> {
    // return an Observable that is treated like EventEmitter
    // because EventEmitter extends Subject, Subject extends Observable
    return new Observable(observer => {
      // this is mostly required because Angular subscribes to the emitter earlier than most of the lifecycle hooks
      // so the chance library is not created yet is quite high
      this.ensureLibraryIsCreated();

      // here we bind to the event. Observables are lazy by their nature, and we fully use it here
      // in fact, the event is getting bound only when Observable will be subscribed by Angular
      // and it will be subscribed only when gets called by the ()-binding
      this.lib.on(eventName, (data: T) => this.ngZone.run(() => observer.next(data)));

      // important what we return here
      // it is quite useful to unsubscribe from particular events right here
      // so, when Angular will destroy the component, it will also unsubscribe from this Observable
      // and this line will get called
      return () => this.lib.off(eventName);
    }) as EventEmitter<T>;
  }

  private ensureLibraryIsCreated() {
    if (!this.lib) {
      this.ngZone.runOutsideAngular(() => this.lib = new MyLib());
    }
  }

}

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

  private createLazyEvent<T>(eventName: string): EventEmitter<T> {
    return this.chartInit.pipe(
      switchMap((chart: ECharts) => new Observable(observer => {
        chart.on(eventName, (data: T) => this.ngZone.run(() => observer.next(data)));
        return null; // no need to react on unsubscribe as long as the `dispose()` is called in ngOnDestroy
      }))
    ) as EventEmitter<T>;
  }

Другие вопросы по теме