AngularJSの$watch、$digest、$applyなど内部の仕組みについて調べた内容をまとめました。
※目次をクリックすると目次の下部にコンテンツが表示されます。
- 1.digestサイクルの概要
- 2.スコープのライフサイクルと$watch、$digest、$apply
- 3.サンプルコードを基に$watch、$digest、$applyの動作を確認
- 4.$applyを手動で実行する必要がある場合の例
digestサイクルの概要
AngularJSがアプリ内のデータ更新を検知するプロセス(AngularJSでは、これをdigestサイクルと呼ぶ)の概要についてまとめました。
①アプリがロードまたはAngularJS内にHTMLがロードされるとAngularJSはコンパイルステップを行い、ng-bindやng-classなどデータの動的変化を検知して動作するHTMLのウォッチャーとリスナーを監視する。
②AngularJSは、それぞれのscopeと結びついているすべての要素についても監視する。
③ユーザーのクリックなどのイベントが起こるとAngularJSはdigestサイクルをトリガーする。
④digestサイクルでは、ブラウザのUIで表示されている値とscopeの現在の値が変化していないかどうか$rootScopeからそれぞれのウォッチャーをチェックする。
⑤変化していなければ次にすべての親のスコープをチェックし、すべてのスコープを確認する。
⑥変化しているスコープがあったらそこで一旦サイクルを停止し、最初からdigestサイクルを繰り返して行う。
ここで再度digestサイクルを最初から行うのは、この変化を検知したウォッチャの変化が他のスコープを変化させている可能性があるので、最初から他のスコープがその後変化していないかチェックする。
⑦上記⑥を繰り返し、変化が検知されなくなるまで繰り返す。一般的なAngularJSアプリでは2~3サイクルで安定する。
無限ループを防ぐため、デフォルトではこのサイクルの上限を10回としている。
⑧digestサイクルが安定したらすべての変更を集積し、即変更をビューに反映する。
①アプリがロードまたはAngularJS内にHTMLがロードされるとAngularJSはコンパイルステップを行い、ng-bindやng-classなどデータの動的変化を検知して動作するHTMLのウォッチャーとリスナーを監視する。
②AngularJSは、それぞれのscopeと結びついているすべての要素についても監視する。
③ユーザーのクリックなどのイベントが起こるとAngularJSはdigestサイクルをトリガーする。
④digestサイクルでは、ブラウザのUIで表示されている値とscopeの現在の値が変化していないかどうか$rootScopeからそれぞれのウォッチャーをチェックする。
⑤変化していなければ次にすべての親のスコープをチェックし、すべてのスコープを確認する。
⑥変化しているスコープがあったらそこで一旦サイクルを停止し、最初からdigestサイクルを繰り返して行う。
ここで再度digestサイクルを最初から行うのは、この変化を検知したウォッチャの変化が他のスコープを変化させている可能性があるので、最初から他のスコープがその後変化していないかチェックする。
⑦上記⑥を繰り返し、変化が検知されなくなるまで繰り返す。一般的なAngularJSアプリでは2~3サイクルで安定する。
無限ループを防ぐため、デフォルトではこのサイクルの上限を10回としている。
⑧digestサイクルが安定したらすべての変更を集積し、即変更をビューに反映する。
スコープのライフサイクルと$watch、$digest、$apply
スコープのライフサイクルにおける$watch、$digest、$applyの役割についてまとめました。
1)rootスコープの生成
・アプリのブートストラップ中に$injectorによって生成される。
・テンプレートのリンク中にいくつかのディレクティブは子スコープを生成する。
2)$watchメソッドを登録
・テンプレートリンク中に、ディレクティブがスコープに$watchメソッドを登録する。
・$watchはモデルの値をDOMに伝達するのに使用される。
3)モデルの更新
・モデルの更新を適切に検知するためには、スコープをscope.$apply()が実行される範囲内で作成する必要がある。
4)モデル更新の監視
・$applyの終わりに、Angularはrootスコープの$digestサイクルを実行し、すべての子スコープを通じて伝達する。
・$digestサイクル中には、すべての$watch対象のexpressionや関数がモデルが更新されていないかチェックされ、モデルが更新されていたら$watchメソッドで設定しているリスナーがコールされる。
5)スコープの破棄
・子スコープが必要なくなった場合は、子スコープcreatorがscope.$destroy()を使って子スコープを破棄する。
・これによって$digestのコールが子スコープへ伝達することがストップされ、子スコープモデルによって使用されていたメモリがGCによって回収・再生(reclaim)される。
1)rootスコープの生成
・アプリのブートストラップ中に$injectorによって生成される。
・テンプレートのリンク中にいくつかのディレクティブは子スコープを生成する。
2)$watchメソッドを登録
・テンプレートリンク中に、ディレクティブがスコープに$watchメソッドを登録する。
・$watchはモデルの値をDOMに伝達するのに使用される。
3)モデルの更新
・モデルの更新を適切に検知するためには、スコープをscope.$apply()が実行される範囲内で作成する必要がある。
4)モデル更新の監視
・$applyの終わりに、Angularはrootスコープの$digestサイクルを実行し、すべての子スコープを通じて伝達する。
・$digestサイクル中には、すべての$watch対象のexpressionや関数がモデルが更新されていないかチェックされ、モデルが更新されていたら$watchメソッドで設定しているリスナーがコールされる。
5)スコープの破棄
・子スコープが必要なくなった場合は、子スコープcreatorがscope.$destroy()を使って子スコープを破棄する。
・これによって$digestのコールが子スコープへ伝達することがストップされ、子スコープモデルによって使用されていたメモリがGCによって回収・再生(reclaim)される。
サンプルコードを基に$watch、$digest、$applyの動作を確認
1)$watchを使ってモデルの変化を検知
<div>私の名前は{{name}}です。</div>
名前:<input ng-model=”name” />
上記2Wayバインディングでは、ビュー内で変化があるとscopeモデルに自動で反映され、scopeモデルが変化するとビューの値が更新されます。
AngularJSの内部では、どのようにこのモデルの変化を検知しているのでしょうか?
下記のように内部で$scopeの$watchメソッドがモデルの値を監視する事によってモデルの更新を検知します。
$scope.$watch(‘name’, function(newValue, oldValue) {
/update the DOM with newValue
});
上記$watchメソッドの二つ目の引数がリスナー関数で、’name’の現在の値が前回チェックした値と一致しているかチェックし、変化していたらリスナー関数が呼び出され、ビュー内の{{name}}更新されます。
2)$watchはいつ呼び出される?
・ある一定の間隔($digestサイクル)で$watchが呼び出されることによって変化をチェックします。
・$digestサイクルは、$scope.$digest()に対するコールの結果として開始されます。
3)$digestはどのようにして呼び出される?
$scope.$apply()によって呼び出されます。
これによって$rootScopeでdigestサイクルが開始され、続いてすべての子スコープの$watchが呼び出されます。
通常は、AngularJSのコントローラやディレクティブ内の同期イベントや$http、$timeout、$intervalサービスを使った非同期処理実施時に自動で$apply()が呼び出されるので明示的に実行する必要はありません。
サードパーティのJavaScriptフレームワークによってモデルの値が変更された場合やイベントを処理する場合は、$digestサイクルが起動されず、モデルの値の変化を検知できないので、明示的に$apply()を実行して$digestサイクルを開始させる必要があります。
<div>私の名前は{{name}}です。</div>
名前:<input ng-model=”name” />
上記2Wayバインディングでは、ビュー内で変化があるとscopeモデルに自動で反映され、scopeモデルが変化するとビューの値が更新されます。
AngularJSの内部では、どのようにこのモデルの変化を検知しているのでしょうか?
下記のように内部で$scopeの$watchメソッドがモデルの値を監視する事によってモデルの更新を検知します。
$scope.$watch(‘name’, function(newValue, oldValue) {
/update the DOM with newValue
});
上記$watchメソッドの二つ目の引数がリスナー関数で、’name’の現在の値が前回チェックした値と一致しているかチェックし、変化していたらリスナー関数が呼び出され、ビュー内の{{name}}更新されます。
2)$watchはいつ呼び出される?
・ある一定の間隔($digestサイクル)で$watchが呼び出されることによって変化をチェックします。
・$digestサイクルは、$scope.$digest()に対するコールの結果として開始されます。
3)$digestはどのようにして呼び出される?
$scope.$apply()によって呼び出されます。
これによって$rootScopeでdigestサイクルが開始され、続いてすべての子スコープの$watchが呼び出されます。
通常は、AngularJSのコントローラやディレクティブ内の同期イベントや$http、$timeout、$intervalサービスを使った非同期処理実施時に自動で$apply()が呼び出されるので明示的に実行する必要はありません。
サードパーティのJavaScriptフレームワークによってモデルの値が変更された場合やイベントを処理する場合は、$digestサイクルが起動されず、モデルの値の変化を検知できないので、明示的に$apply()を実行して$digestサイクルを開始させる必要があります。
$applyを手動で実行する必要がある場合の例
AngularのビルトインのディレクティブなどAngularJSのコンテキスト内でモデルの変化がある場合には$applyが自動で実行されますが、コンテキスト外の場合はモデルの変化を検知できず、$applyが実行されません。この場合は、$apply()を手動で実行する必要があります。
AngularJSの$timeoutサービスとJavaScriptのsetTimeout()関数で遅延実行する場合のサンプルを例に$apply()を手動で実行する必要がある場合の例を示します。
1)AngularJSの$timeoutサービスを使ったサンプル
下記サンプルコードでは、AngularJSの$timeoutサービスを使用しているので$timeoutサービスで遅延実行した後に自動で$apply()が呼び出され、$scope.msgの値が更新されてビューに表示されます。
AngularJSの$timeoutサービスとJavaScriptのsetTimeout()関数で遅延実行する場合のサンプルを例に$apply()を手動で実行する必要がある場合の例を示します。
1)AngularJSの$timeoutサービスを使ったサンプル
下記サンプルコードでは、AngularJSの$timeoutサービスを使用しているので$timeoutサービスで遅延実行した後に自動で$apply()が呼び出され、$scope.msgの値が更新されてビューに表示されます。
<script> var demoApp = angular.module('demoApp', []); demoApp.controller('demoCtrl', function ($scope,$timeout) { $scope.getMsg = function() { $timeout(function(){ $scope.msg = '遅延実行完了'; console.log('$timeout使用:'+$scope.msg); }, 2000); } $scope.getMsg(); }); </script> <body ng-controller="demoCtrl"> <span>$timeout使用:{{msg}}</span> </body>
2)JavaScriptのsetTimeout()関数を使用
AngularJSの$timeoutサービスではなく、JavaScriptのsetTimeout()関数を使用しているので、自動ではモデルの変化を検知できず、$apply()が自動で実行されません。ビュー内の{{msg}}は更新されずビューにメッセージが表示されません。
$scope.getMsg = function() { setTimeout(function() { $scope.msg = '遅延実行完了'; console.log('setTimeoutで$apply使用せず:'+$scope.msg); }, 2000); } $scope.getMsg();
3)setTimeout()関数で$apply()を使用
手動で$apply()を実行する事によってビュー内の{{msg}}は更新され、ビューに表示されます。
$scope.getMsg = function() { setTimeout(function() { $scope.$apply(function() { $scope.msg = '遅延実行完了'; console.log('setTimeoutで$apply使用:' + $scope.msg); }); }, 2000); } $scope.getMsg();
AngularJSの特徴、仕組み
ビルトインディレクティブ
- ビルトインのディレクティブの概要
- データバインディングのディレクティブの概要
- ng-repeatを使ってリスト、テーブル、フィルター
- ng-includeを使って別のHTMLファイルを埋め込み
- ng-switchを使って内部にあるHTMLテンプレートの表示を切り替え
- ng-if、ng-show、ng-hideを使って表示、非表示を切り替え
- ng-class、ng-styleを使ってCSSを設定
- イベント処理
- 有効、無効を切替えるディレクティブ
- フォームの入力チェックを行い、動的にCSS追加
- ng-optionsを使ってSelectメニューを作成
- ng-repeatを使ってラジオボタンを作成
- ビルトインのフィルターの使用方法
- ビルトインのフィルターで関数を使用
- カスタムフィルターの使用方法
- Bootstrap3を使ってページング
- スクリプト内でフィルターを使用、既存のフィルターを拡張
サービス
- サービスの概要とfactory、service、providerの使い分け
- $qサービスでpromise/deferredパターン
- $httpサービスの概要、サーバーからJSONデータ取得
- $httpサービスのPOSTでデータ送信
- $qサービスを使ってhttpリクエストを複数実行
- $httpサービスのinterceptorの使用方法
- AngularJSのwithCredentialsの設定、CORS
- $timeoutサービスと$intervalサービス
- $resourceサービスを使ってREST
- $locationサービスを使ってURLを操作
- $routeサービスと$resourceサービスを使ったサンプルを作成
- routeChangeSuccessを使ってflashメッセージ表示
- JavaScript、CSSを含むHTMLデータをバインドして表示
カスタムディレクティブ
AngularJSの特徴、仕組み
ビルトインディレクティブ
- ビルトインのディレクティブの概要
- データバインディングのディレクティブの概要
- ng-repeatを使ってリスト、テーブル、フィルター
- ng-includeを使って別のHTMLファイルを埋め込み
- ng-switchで内部にあるHTMLテンプレートの表示を切り替え
- ng-if、ng-show、ng-hideを使って表示、非表示を切り替え
- ng-class、ng-styleを使ってCSSを設定
- イベント処理
- 有効、無効を切替えるディレクティブ
- フォームの入力チェックを行い、動的にCSS追加
- ng-optionsを使ってSelectメニューを作成
- ng-repeatを使ってラジオボタンを作成
- ビルトインのフィルターの使用方法
- ビルトインのフィルターで関数を使用
- カスタムフィルターの使用方法
- Bootstrap3を使ってページング
- スクリプト内でフィルターを使用、既存のフィルターを拡張
サービス
- サービスの概要とfactory、service、providerの使い分け
- $qサービスでpromise/deferredパターン
- $httpサービスの概要、サーバーからJSONデータ取得
- $httpサービスのPOSTでデータ送信
- $qサービスを使ってhttpリクエストを複数実行
- $httpサービスのinterceptorの使用方法
- AngularJSのwithCredentialsの設定、CORS
- $timeoutサービスと$intervalサービス
- $resourceサービスを使ってREST
- $locationサービスを使ってURLを操作
- $routeサービスと$resourceサービスを使ったサンプルを作成
- routeChangeSuccessを使ってflashメッセージ表示
- JavaScript、CSSを含むHTMLデータをバインドして表示
カスタムディレクティブ