最佳从 EventEmitter 事件重新进入 ngZone
Optimal reentering the ngZone from EventEmitter event
有一个组件封装了一些库。为了避免所有这个库的事件侦听器的变更检测噩梦,库的范围在 angular 区域之外:
@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。
请记住,根据定义,发出值的 @Output()
绑定是父级更改检测的触发器。虽然该绑定可能没有任何侦听器,但在引用该组件的父模板中可能存在逻辑。也许通过 exportAs
或 @ViewChild
查询。因此,如果您发出一个值,您就是在通知父级组件的状态已更改。也许将来 Angular 团队会改变这一点,但这就是目前的工作方式。
如果您想绕过该可观察对象的更改检测,请不要使用 @Output
装饰器。删除装饰器并通过 exportAs
访问 emtter
属性 或在父组件中使用 @ViewChild
。
看看响应式表单是如何工作的。控件的指令具有 public 可观察值,用于不使用 @Output
的更改。它们只是 public 个可观察对象,您可以订阅它们。
因此,如果您想要一个不与变化检测耦合的可观察对象,那么只需将其设为 public 的可观察对象即可。这只是保持简单。添加仅在 @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();
});
}
}
}
如果我以后看到这段源码,我会有点疑惑程序员为什么要这么做。它添加了很多额外的逻辑,这些逻辑并不能清楚地解释逻辑正在解决的问题。
首先感谢cgTag
的回答。它引导我进入更好的方向,它更具可读性、使用起来更舒适,而不是 getter 使用 Observable 自然惰性。
这里有一个很好解释的例子:
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());
}
}
}
这是另一个示例,其中使用了库实例 observable(每次重新创建时都会发出库实例,这是很常见的情况):
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>;
}
有一个组件封装了一些库。为了避免所有这个库的事件侦听器的变更检测噩梦,库的范围在 angular 区域之外:
@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。
请记住,根据定义,发出值的 @Output()
绑定是父级更改检测的触发器。虽然该绑定可能没有任何侦听器,但在引用该组件的父模板中可能存在逻辑。也许通过 exportAs
或 @ViewChild
查询。因此,如果您发出一个值,您就是在通知父级组件的状态已更改。也许将来 Angular 团队会改变这一点,但这就是目前的工作方式。
如果您想绕过该可观察对象的更改检测,请不要使用 @Output
装饰器。删除装饰器并通过 exportAs
访问 emtter
属性 或在父组件中使用 @ViewChild
。
看看响应式表单是如何工作的。控件的指令具有 public 可观察值,用于不使用 @Output
的更改。它们只是 public 个可观察对象,您可以订阅它们。
因此,如果您想要一个不与变化检测耦合的可观察对象,那么只需将其设为 public 的可观察对象即可。这只是保持简单。添加仅在 @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();
});
}
}
}
如果我以后看到这段源码,我会有点疑惑程序员为什么要这么做。它添加了很多额外的逻辑,这些逻辑并不能清楚地解释逻辑正在解决的问题。
首先感谢cgTag
的回答。它引导我进入更好的方向,它更具可读性、使用起来更舒适,而不是 getter 使用 Observable 自然惰性。
这里有一个很好解释的例子:
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());
}
}
}
这是另一个示例,其中使用了库实例 observable(每次重新创建时都会发出库实例,这是很常见的情况):
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>;
}