Edited at

Angular2はいかにしてオブジェクトの変更を監視しているのか

More than 3 years have passed since last update.

こんにちは、laco0416です。

今回はAngular2がいかにしてオブジェクトの変更を監視し、データバインディングを解決しているのかを解き明かします。


結論

この部分でループとtick処理を実装していた。

ObservableWrapper.subscribe(this._zone.onTurnDone,

(_) => { this._zone.run(() => { this.tick(); }); });


調査開始

Angular2は$applyがないのにどうやってオブジェクトの変更をビューに反映しているんだろう?という疑問から調査を開始。

そもそも、Componentのプロパティに変更を加えたときに何かイベントが発生しているわけではない(object.ObserveもProxiesも使っていない)ので、何かしらのタイミングで別のメソッドから変更があるかどうかをチェックしているはず。

ということで変更を検知する処理を探索、AbstractChangeDetectorにdetectChangesメソッドを発見。

https://github.com/angular/angular/blob/fcc7ce225ec6b6abc8935c2a024941ee53dce1e6/modules/angular2/src/core/change_detection/abstract_change_detector.ts#L76

  detectChanges(): void { this.runDetectChanges(false); }

このメソッドが呼ばれると、ChangeDetectorが保存している状態と現在の状態を比較して、変更点をリストアップするらしい。

次にこのdetectChangesが呼ばれている部分を探す。発見。

https://github.com/angular/angular/blob/7ae23adaff2990cf6022af9792c449730d451d1d/modules/angular2/src/core/application_ref.ts#L471

  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()メソッドの中で呼ばれていた。ざっと上から処理を追うと、


  1. tickが入れ子になっていないかのチェック(1ApplicationRefにつき同時に走るtickは1つ)


  2. _tickScopeの呼び出し。中はプロファイリング用の処理だった。無視してOK

  3. tick処理を開始。フラグを立てる

  4. ApplicationRefが持っているChangeDetectorすべてにdetectChangesを実行


  5. _enforceNoNewChangesがtrueならすべてのChangeDetectorを変更がなかったものとする(ngAfter**系のライフサイクルが発生しないっぽい)

  6. tick処理を終了。フラグを下ろす

  7. プロファイリングを終了する。無視してOK

アプリケーション全体のデータバインディングを解決するメソッドが分かった。これがAngularJSの$digestループ相当のものらしい。あとはこれが呼ばれている場所がわかればいい。

というわけでtick()を呼び出している部分を探索、発見。

https://github.com/angular/angular/blob/7ae23adaff2990cf6022af9792c449730d451d1d/modules/angular2/src/core/application_ref.ts#L374

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である。

class NgZone

どのように拡張しているかというと、

Zoneのrunが実行されるたびに自身のonTurnStartを発火し、処理が終了するとonTurnDoneを発火するようになっている。

このソースにある_notifyOnTurnStart_notifyOnTurnDoneがそれである。


this._zone.run(() => { this.tick(); }

これはApplicationRefが持っているZone中でtick処理を行っているだけである。Zoneについては本稿では扱わないが、複数の非同期処理をグループ化し、コンテキストを共有したもののように思ってもらえればよい。同じZone内で起きたエラーを一括でハンドルしたり、非同期のスタックトレースを取得できたりする。

angular/zone.js: Implements Zones for JavaScript

これですべての謎が解けた。まとめると以下のようになる。


  1. ApplicationRefが作成される(bootstrap関数の中で作られる)

  2. ApplicationのNgZoneが作成され、tickループが作られる

  3. 各Componentが自身のChangeDetectorをApplicationに登録する(これはコンポーネントツリー構築時にされている)

  4. tickが呼ばれる

  5. すべてのChangeDetectorが変更チェックし、データバインディングを解決する

  6. tick処理が終わるとonTurnDoneイベントが発火する


  7. onTurnDoneイベントを受けてtickを実行する

  8. 4に戻る

イベントドリブンな再帰ループ?とでも言うのだろうか。ともかくこういう仕組みで動いている。setIntervalとかではない。


所感

RxJSとZone.jsとの合わせ技だが、わかってしまえばシンプルだった。ちなみに処理の追跡は全部GitHub上で出来たので楽だった。

Zone.jsについてはまた後日記事を書こうと思う。