今回は 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 についてはまた後日記事を書こうと思う。