AngularJSの$watch、$digest、$applyの概要

AngularJSの$watch、$digest、$applyなど内部の仕組みについて調べた内容をまとめました。

(1)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サイクルが安定したらすべての変更を集積し、即変更をビューに反映する。
 

(2)スコープのライフサイクルと$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)される。
 

(3)サンプルコードを基に$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サイクルを開始させる必要があります。
 

(4)$applyを手動で実行する必要がある場合の例


 
AngularのビルトインのディレクティブなどAngularJSのコンテキスト内でモデルの変化がある場合には$applyが自動で実行されますが、コンテキスト外の場合はモデルの変化を検知できず、$applyが実行されません。この場合は、$apply()を手動で実行する必要があります。
 
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();
関連記事の目次

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です