By

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がどのようにブラウザのイベントループによって作用しているかを説明したものです。

concepts-runtime

まず、ブラウザのイベントループについて説明します。ブラウザのイベントループは、

  1. イベントが到達するのを待ち受ける。イベントには、ユーザー操作、タイマーイベント、ネットワークイベント(サーバからのレスポンス)がある。
  2. イベントのコールバックが実行され、JavaScriptのコンテキストに入る。コールバックはDOM構造を変更することが可能。
  3. 1度コールバックが実行されると、ブラウザはJavaScriptコンテキストを残し、DOMの変更を元にしたビューを再描画する。

の様になります。Angularは、この標準のJavaScriptフローを自身のイベント処理ループを割り当てることで修正します。

では、実際にどのように動作していくのかを、データバインディングによる反映方法(下記を例として)を用いて説明します。

<div ng-controller="MyController">
  Your name: <input type="text" ng-model="name">
  <hr>
  {{name}}
</div>

1. コンパイルフェーズ

  1. ng-modelとinputディレクティブに、input制御上でのkeydownリスナーを設定。
  2. interpolationによって、nameの変更が$watchに通知されるように設定される。

2. ランタイムフェーズ

  1. input上で'X'キーを押すと、ブラウザがkeydownイベントを発生させる。
  2. inputディレクティブは、inputの値の変更を捕捉し、Angular実行コンテキスト内部のアプリケーションモデルを変更するために$apply("name = 'X';")を呼び出す。
  3. Angularはname = 'X';をモデルに対し適用。
  4. $digestループを開始。
  5. $watchリストはnameプロパティの変更を見つけ、interpolationに通知しDOMを更新して変更するように伝える。
  6. Angularはコンテキストの実行を終了し、keydownイベントとそのJavaScriptのコンテキストの実行が終了する。
  7. ブラウザはテキストの更新で再描画する。

このように$apply, $digestを通して変更を行っていきます。
 
ここで気になるのが、Angularを使わないライブラリでDOMの操作を行った時に、どうすればいいかということです。 この場合は、上記に書いたように、$applyを使うべきなんですが、その説明は次回にしたいと思います。


エンジニア募集中!

ビジネスバンクグループではエンジニアを募集中しています。

弊社が採用しているテクノロジや開発環境に興味を持った方は、 ここから是非エントリー を!