当在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