こんにちは、laco0416です。
今回はAngular2がいかにしてオブジェクトの変更を監視し、データバインディングを解決しているのかを解き明かします。
結論
この部分でループとtick処理を実装していた。
ObservableWrapper.subscribe(this._zone.onTurnDone,
(_) => { this._zone.run(() => { this.tick(); }); });
調査開始
Angular2は$apply
がないのにどうやってオブジェクトの変更をビューに反映しているんだろう?という疑問から調査を開始。
そもそも、Componentのプロパティに変更を加えたときに何かイベントが発生しているわけではない(object.ObserveもProxiesも使っていない)ので、何かしらのタイミングで別のメソッドから変更があるかどうかをチェックしているはず。
ということで変更を検知する処理を探索、AbstractChangeDetectorにdetectChanges
メソッドを発見。
detectChanges(): void { this.runDetectChanges(false); }
このメソッドが呼ばれると、ChangeDetectorが保存している状態と現在の状態を比較して、変更点をリストアップするらしい。
次にこのdetectChanges
が呼ばれている部分を探す。発見。
tick(): void {
if (this._runningTick) {
throw new BaseException("ApplicationRef.tick is called recursively");
}
var s = ApplicationRef_._tickScope();
try {
this._runningTick = true;
this._changeDetectorRefs.forEach((detector) => detector.detectChanges());
if (this._enforceNoNewChanges) {
this._changeDetectorRefs.forEach((detector) => detector.checkNoChanges());
}
} finally {
this._runningTick = false;
wtfLeave(s);
}
}
ApplicationRef_
クラスのtick()メソッドの中で呼ばれていた。ざっと上から処理を追うと、
- tickが入れ子になっていないかのチェック(1ApplicationRefにつき同時に走るtickは1つ)
-
_tickScope
の呼び出し。中はプロファイリング用の処理だった。無視してOK - tick処理を開始。フラグを立てる
- ApplicationRefが持っているChangeDetectorすべてに
detectChanges
を実行 -
_enforceNoNewChanges
がtrueならすべてのChangeDetectorを変更がなかったものとする(ngAfter**
系のライフサイクルが発生しないっぽい) - tick処理を終了。フラグを下ろす
- プロファイリングを終了する。無視してOK
アプリケーション全体のデータバインディングを解決するメソッドが分かった。これがAngularJSの$digestループ相当のものらしい。あとはこれが呼ばれている場所がわかればいい。
というわけでtick()を呼び出している部分を探索、発見。
constructor(private _platform: PlatformRef_, private _zone: NgZone, private _injector: Injector) {
super();
if (isPresent(this._zone)) {
ObservableWrapper.subscribe(this._zone.onTurnDone,
(_) => { this._zone.run(() => { this.tick(); }); });
}
this._enforceNoNewChanges = assertionsEnabled();
}
ApplicationRef_
のコンストラクタである。bootstrap関数によってアプリケーションの開始時に一度だけ呼ばれる部分。当たり前といえば当たり前である。
とはいえ初見ではこれがtickループの実装だとはわからないと思うので、ひとつずつ解説する。
ObservableWrapper.subscribe
ObservableWrapperの実装はこれ
class ObservableWrapper
RxJSのObservableをラップし、EventEmitterと協調するためのAngular2用の非同期処理用便利クラスである。Observableの処理をWrapperのstaticメソッドで行うことができるのでRxJSを隠蔽できる。
subscribe
メソッドは、第1引数に渡されたEventEmitterのイベントが発行されるたびに第2引数の関数が実行される。
this._zone.onTurnDown
subscribeの第1引数に渡されたこれは前述のとおりEventEmitterである。つまり、このイベントが発火されるたびに第2引数の処理が走る。
this._zone
の型はNgZone
だが、これはZone.jsのZoneを拡張したAngular2用のZoneである。
どのように拡張しているかというと、
Zoneのrun
が実行されるたびに自身のonTurnStart
を発火し、処理が終了するとonTurnDone
を発火するようになっている。
このソースにある_notifyOnTurnStart
と_notifyOnTurnDone
がそれである。
this._zone.run(() => { this.tick(); }
これはApplicationRefが持っているZone中でtick処理を行っているだけである。Zoneについては本稿では扱わないが、複数の非同期処理をグループ化し、コンテキストを共有したもののように思ってもらえればよい。同じZone内で起きたエラーを一括でハンドルしたり、非同期のスタックトレースを取得できたりする。
angular/zone.js: Implements Zones for JavaScript
これですべての謎が解けた。まとめると以下のようになる。
- ApplicationRefが作成される(bootstrap関数の中で作られる)
- ApplicationのNgZoneが作成され、tickループが作られる
- 各Componentが自身のChangeDetectorをApplicationに登録する(これはコンポーネントツリー構築時にされている)
- tickが呼ばれる
- すべてのChangeDetectorが変更チェックし、データバインディングを解決する
- tick処理が終わると
onTurnDone
イベントが発火する -
onTurnDone
イベントを受けてtickを実行する - 4に戻る
イベントドリブンな再帰ループ?とでも言うのだろうか。ともかくこういう仕組みで動いている。setIntervalとかではない。
所感
RxJSとZone.jsとの合わせ技だが、わかってしまえばシンプルだった。ちなみに処理の追跡は全部GitHub上で出来たので楽だった。
Zone.jsについてはまた後日記事を書こうと思う。