当在Angular外部发生事件时,如何检测变化

我在株式会社ゴーガ工作,他们是从2017年9月开始成为Google的高级合作伙伴。我是一名工程师,使用Google Maps API和Angular开发与地图有关的Web服务。

由于我特别想不到任何主题,所以我将写一些关于我最近钟情的事情。我想提醒大家在使用像Google地图这样的第三方库时要注意的事项。

首先

在点击Google地图上的标记(显示在地图上的指示)时,我正在编写一段代码,根据标记的信息更新Angular组件。但是,我遇到了一个问题,即无法正确打开Angular Material的对话框。在进行了各种调查后,我意识到当点击标记时,组件的ngAfterViewChecked没有任何反应(视图未更新)。

总结起来,就是Google地图的点击事件是在Angular控制范围外的,所以无论点击多少次标记,Angular的变化检测器都不会做出响应。

之前是如何实施的?

因为实际的代码中包含了不必要的内容,所以我们将用一个简单的例子来进行解释。

假设存在一个用于显示Google Map的服务(google-map.service.ts),如下所示。通过调用initMap,可以初始化地图。通过调用addMarker,可以根据传入的标记信息在地图上显示标记。

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
...

@Injectable()
export class GoogleMapService {
  map: any;
  markerClick: Subject<Marker> = new Subject<Marker>();

  /**
   * 地図を初期化
   */
  initMap() {
    this.map = new google.maps.Map(document.getElementById('google-maps'), {
      zoom: 5,
      mapTypeId: google.maps.MapTypeId.ROADMAP,
      center: { ... },
    });
  }

  /**
   * マーカーを地図に表示
   */
  addMarker(markerData: MarkerData) {
    markerData.map(marker => {
      const gMarker = new google.maps.Marker({
        position: { ... },
        icon: { ... },
        map: this.map,
      });

      // マーカーをクリックした時の処理(今回のテーマのメインの部分)
      gMarker.addListener('click', () => this.markerClick.next(marker));
    });
  }

  ...

}

当您点击地图上显示的标记时,处理方式如下。

gMarker.addListener('click', () => this.markerClick.next(marker));

当markerClick作为Subject时,当点击标记时,标记信息将流入流中。因此,如果在组件中进行Service的DI处理,应该能够接收标记信息,并使用该信息来更新视图等操作。

然而,我想已经有人注意到了,这样做将无法实现组件的变更检测(Change Detector)功能。假设我们向组件添加了以下内容。无论点击地图标记多少次,它都不会有任何反应。

ngAfterViewChecked() {
  console.log('ngAfterViewChecked'); // <= 全く反応なし...
}

如何进行更改(解决方式:使用NgZone)

经过各种调查研究,我们最终在以下方案中找到了解决办法。

import { Injectable, NgZone } from '@angular/core';
import { Subject } from 'rxjs/Subject';
...

@Injectable()
export class GoogleMapService {
  map: any;
  markerClick: Subject<Marker> = new Subject<Marker>();

  constructor(
    private ngZone: NgZone,
  ) { }

  /**
   * 地図を初期化
   */
  initMap() {
    this.map = new google.maps.Map(document.getElementById('google-maps'), {
      zoom: 5,
      mapTypeId: google.maps.MapTypeId.ROADMAP,
      center: { ... },
    });
  }

  /**
   * マーカーを地図に表示
   */
  addMarker(markerData: MarkerData) {
    markerData.map(marker => {
      const gMarker = new google.maps.Marker({
        position: { ... },
        icon: { ... },
        map: this.map,
      });

      // マーカーをクリックした時の処理(今回のテーマのメインの部分)
      gMarker.addListener('click', () => {
        this.ngZone.run(() => {
          this.markerClick.next(marker);
        });
      });
    }
  }

  ...

}

我的做法是使用Angular的模块NgZone,并在点击标记时调用NgZone的run()方法来处理。通过这样做,可以将标记点击的处理放置在Angular的管辖范围内,并且可以启用Angular的变更检测处理。

“NgZone是什么?”

Angular有一个内部组件称为Zone,Zone中被打补丁的是一些异步API,比如addEventListener和setTimeout。Angular会在Zone内执行组件代码。通过这种方式,Angular能够知道何时发生了异步操作,并能执行变更检测(Change Detection)。

相反地,如果在Zone的外部发生异步处理,就像这个例子一样,Angular将无法知道异步处理发生的情况,也不会执行变更检测(Change Detection)。

NgZone是一个用于使该Zone在Angular中更易于处理的模块,这样说很容易理解吧。

    https://angular.io/api/core/NgZone

通过使用NgZone的run()方法,您可以在Zone的内部明确地执行代码,并且可以激活Angular的变更检测(Change Detection)。

gMarker.addListener('click', () => {
  this.ngZone.run(() => {
    this.markerClick.next(marker);
  });
});

关于 Zone 和 NgZone,以下的文章可以作为参考。

    • AngularとZone.jsとテストの話 – Qiita

 

    • 日本語訳:Angular 2 Change Detection Explained – Qiita

 

    • Angularでイベントから無駄にChange Detectionを走らせないためにすべきこと

 

    Zones in Angular by thoughtram

在Angular中使用detectChanges()不行吗?

在Angular中,还有一个名为ChangeDetectorRef的方法,它可以显式地触发Angular的变更检测(Change Detection)。这个方法叫做detectChanges()。

    https://angular.io/api/core/ChangeDetectorRef
markerClick(event: any) {
  this.position = event.position;
  this.cd.detectChanges();
}

在更新组件属性后,调用detectChanges()是没有问题的。但是,如果在其中加入诸如调用API等处理时,需要注意的是,在异步处理完之前就执行detectChanges(),可能导致无法正确进行变更检测。

markerClick(event: any) {
  this.store.dispatch(new CallApi(event)); // <= 非同期処理を伴う
  this.cd.detectChanges();
}

如果不希望对Angular进行更改检测的话

使用run()方法来在Zone内部执行的代码,我有时候希望在Zone外部再次执行,或者从一开始就不想在Zone内部执行。例如,当我在浏览器的“返回”或“前进”按钮上进行导航时,Angular会对Google Map的mousemove和标记动画做出反应,导致ngAfterViewChecked被调用,从而引发了无限循环的奇怪现象。

尽管原因无法确定,但通过在NgZone的runOutsideAngular()方法中调用初始化Google Map的方法来避免了这个问题。

this.ngZone.runOutsideAngular(() => this.googleMapService.initMap());

“runOutsideAngular()方法可以被视为run()方法的反向版本。它可以在Zone的外部执行代码,并且不会触发Angular的变更检测(Change Detection)机制。”

总结

我开始使用Angular大约三个月了,但是我仍然觉得变更检测方面有些困难。这还只是在试验和探索阶段的内容。除了使用Google Maps,我还可能会使用认证系统、图表以及其他外部库。当在使用这些外部库时,希望能对你们有所帮助。

请参考

    • https://blog.angularindepth.com/do-you-still-think-that-ngzone-zone-js-is-required-for-change-detection-in-angular-16f7a575afef

 

    • https://stackoverflow.com/questions/44079424/ionic-2-google-maps-marker-click

 

    • https://stackoverflow.com/questions/31352397/how-to-update-view-after-change-in-angular2-after-google-event-listener-fired

 

    • https://stackoverflow.com/questions/37148813/angular-2-why-do-i-need-zone-run

 

    • https://stackoverflow.com/questions/41364386/whats-the-difference-between-markforcheck-and-detectchanges

 

    https://stackoverflow.com/questions/37643607/in-angular2-advantage-of-using-zone-run-vs-changedetecotor-markforcheck/37643737#37643737
bannerAds