代码之家  ›  专栏  ›  技术社区  ›  smnbbrv

事件发射器事件中ngZone的最佳重入

  •  3
  • smnbbrv  · 技术社区  · 8 年前

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

    @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 回复  |  直到 8 年前
        1
  •  3
  •   Reactgular    8 年前

    记住 @Output() 根据定义,发出值的绑定是父级更改检测的触发器。虽然该绑定可能没有任何侦听器,但父模板中可能有引用该组件的逻辑。也许是通过 exportAs 或者 @ViewChild 查询。因此,如果您发出一个值,您将通知父组件的状态已更改。也许将来角度团队会改变这一点,但这就是它目前的工作方式。

    如果你想通过观察到的变化检测,那么不要使用 @Output 装饰工。移除装饰器并访问 emtter 通过 出口 或者使用 @视童 在父组件中。

    看看反应式表单是如何工作的。控件的指令对不使用 @输出 . 它们只是公众可观察到的,你可以订阅它们。

    所以,如果你想拥有一个不与变化检测耦合的可观测数据,那么就让它成为一个公开的可观测数据。这样就简单了。仅当存在 @输出 当您稍后阅读源代码时,会使组件难以理解。

    话虽如此,这就是我如何回答你的问题,以便你能利用 @输出() 只有当有订户时。

    @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();
                });
            }
        }
    }
    

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

        2
  •  3
  •   smnbbrv    7 年前

    首先,感谢 cgTag 的答案。它引导我进入一个更好的方向,更可读,更舒适的使用,而不是getter使用可观察到的自然懒惰。

    下面是一个解释得很好的例子:

    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());
        }
      }
    
    }
    

    下面是另一个例子,其中使用了库实例observate(它在每次重新创建库实例时都会发出该实例,这是非常常见的场景):

      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>;
      }
    
    推荐文章