从EventEmitter事件中最佳重新进入ngZone

时间:2018-08-10 13:48:10

标签: angular typescript output ngzone angular-event-emitter

有一个封装了某些库的组件。为了避免所有此类库的事件侦听器发生更改检测的噩梦,该库的作用域位于角度区域之外:

@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>

这是不需要的,因为,我没有订阅该事件=>没有要检测的东西。

该问题的解决方案是什么?

对于那些对真实示例感兴趣的人,问题的源头是here

2 个答案:

答案 0 :(得分:3)

请记住,根据定义,发出值的@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();
            });
        }
    }
}

如果我以后会看到此源代码,那么对于程序员为什么这样做会感到有些困惑。它添加了很多额外的逻辑,无法清楚地解释逻辑正在解决的问题。

答案 1 :(得分:3)

首先,感谢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>;
  }