AngularJS ScopeのLife Cycleについて
ビジネスバンクグループエンジニアの 栗山 宗久 です。
AngularJSの要であるscopeのライフサイクルについて、公式ドキュメントを要約しつつ、確認していきたいと思います。
https://docs.angularjs.org/guide/scope
Scopeとは?
そもそもスコープとは、アプリケーションのコントローラーとビューを繋げる糊の役割を果たします。特徴としては、
- モデルの変更を見張るAPI($watch)を提供
- "Angular領域"(コントローラー、サービス、Angularイベントハンドラ)の外部から、 ビューへシステムを通してモデル変更を伝搬するためのAPI($apply)を提供
- 共有モデルのプロパティへのアクセスを提供しつつ、アプリケーションの分離コンポーネントを入れ子にすることができる。 スコープ(のプロトタイプ)は、その親スコープからプロパティを継承する
- 評価対象の式 に対してコンテキストを提供。 例えば、式は、usernameプロパティが定義された特定のスコープに対して評価される
となります。 ここで注目するのが"$watch"の提供です。モデルの変更を監視する$watchに監視したいモデルを登録することにより、モデルの変更に対応できます。
Scopeのライフサイクル
Scopeのライフサイクルは、次の手順で実行されます。
1. Creation
アプリケーションの起動中に、$injectorによってルートスコープが作成されます。 そして、テンプレート(View)のリンク中に、ディレクてイプはそれぞれ新しい子スコープを作成します。
2. Watcher registration
テンプレート(View)のリンク中に、ディレクティブはスコープにwatchesを登録していきます。これらのwatchesは、DOMへモデルの値を伝える際に使用されます。
3. Model mutation
値の変更を適切に監視するためには、scope.$apply()内で値の変更を実行すべきです。これはAngular実行コンテキストの外部で、ブラウザがJavaScriptコードの実行を呼び出す場合、 Angularがモデルの変更を認識することが出来ないからです。
適切にモデルの変更を処理するためには、$apply()メソッドを使用して、Angularの実行コンテキストに入れる必要があります。$apply()メソッド内で実行されたモデル変更のみが、Angularによって適切に処理されます。
例えば、もしディレクティブがng-clickのような、DOMイベントをリッスンしていた場合、$apply()メソッド内で式を評価しなければいけません。
AngularのAPIはこれを暗黙的に行うため、コントローラー内での同期処理、または$httpや$timeout等のサービスでの非同期処理の際に$applyを呼び出す必要はありません。
4. Mutation observation
$applyの最後に、Angularはルートスコープ上で$digestサイクルを実行し、全ての子スコープに伝搬します。$digestサイクル中は、全ての$watch式または関数はモデルの変更を確認し、もし変更されていれば$watchリスナーが呼び出されます。
5. Scope destruction
子スコープが不要になった場合、scope.$destroy()のAPIを介してそれらを削除するのは、子スコープ作成者の責務です。これにより、子スコープの$digest呼び出しによる伝搬を停止し、ガベージコレクタが、子スコープモデルに使用されていたメモリを開放出来るようにします。
次にこのScopeのライフサイクルがどのように、ブラウザのイベントループと統合されるかを見ていきます。
ブラウザーのイベントループとの統合
次の図(公式ドキュメントより引用)は、Angularがどのようにブラウザのイベントループによって作用しているかを説明したものです。
まず、ブラウザのイベントループについて説明します。ブラウザのイベントループは、
- イベントが到達するのを待ち受ける。イベントには、ユーザー操作、タイマーイベント、ネットワークイベント(サーバからのレスポンス)がある。
- イベントのコールバックが実行され、JavaScriptのコンテキストに入る。コールバックはDOM構造を変更することが可能。
- 1度コールバックが実行されると、ブラウザはJavaScriptコンテキストを残し、DOMの変更を元にしたビューを再描画する。
の様になります。Angularは、この標準のJavaScriptフローを自身のイベント処理ループを割り当てることで修正します。
では、実際にどのように動作していくのかを、データバインディングによる反映方法(下記を例として)を用いて説明します。
<div ng-controller="MyController">
Your name: <input type="text" ng-model="name">
<hr>
{{name}}
</div>
1. コンパイルフェーズ
- ng-modelとinputディレクティブに、input制御上でのkeydownリスナーを設定。
- interpolationによって、nameの変更が$watchに通知されるように設定される。
2. ランタイムフェーズ
- input上で
'X'
キーを押すと、ブラウザがkeydownイベントを発生させる。 - inputディレクティブは、inputの値の変更を捕捉し、Angular実行コンテキスト内部のアプリケーションモデルを変更するために
$apply("name = 'X';")
を呼び出す。 - Angularは
name = 'X';
をモデルに対し適用。 - $digestループを開始。
- $watchリストはnameプロパティの変更を見つけ、interpolationに通知しDOMを更新して変更するように伝える。
- Angularはコンテキストの実行を終了し、keydownイベントとそのJavaScriptのコンテキストの実行が終了する。
- ブラウザはテキストの更新で再描画する。
このように$apply, $digestを通して変更を行っていきます。
ここで気になるのが、Angularを使わないライブラリでDOMの操作を行った時に、どうすればいいかということです。
この場合は、上記に書いたように、$applyを使うべきなんですが、その説明は次回にしたいと思います。
エンジニア募集中!
ビジネスバンクグループではエンジニアを募集中しています。
弊社が採用しているテクノロジや開発環境に興味を持った方は、 ここから是非エントリー を!