AngularJSの$apply, $digest, $watch についての整理
はじめに
AngularJSは独自の概念が多いので慣れるまでは少し苦労することだろうと思います。
理解するのに苦労する独自の概念の一つに、$apply、 $digest、$watchなどのデータバインディング 関連の仕組みがあります。
今回、これらの概念について整理するためにもまとめてみようと思います。
詳しくは公式ドキュメント等を見たほうがよいと思います。
AngularJSのデータバインディング
AngularJSではHTMLなどのテンプレートと中の任意の値と$scopeを関連づけていて、それをデータバインディングと言います。
$scopeはcontorollerで管理されていてすごく便利な仕組みです。
具体的に何が便利かというと、
- DOM(モデル)の変更を自動で検知して、自動で$scopeに反映してくれる
- controllerで$scopeを操作すると、その変更を自動でDOMに反映してくれる
という二点だと思います。
詳しいイメージ図は、調べれば色々と出てくると思うのでここでは省略しますが、これがとても便利なのです。
データバインディングの仕組みなしでやろうとすると、DOMを取得して、イベントハンドラを設定して、DOMを弄りまくって、など色々しないといけないのですが、ちょっと複雑なことをしようとすると、途端にぐしゃぐしゃになってしまい管理できなくなってしまうのです。
(実力が足りないだけかもしれない)
とにかく、このぐしゃぐしゃになってしまうところを$scopeを使った仕組みで綺麗に抽象化してくれているのがAngularJSのデータバインディングです。
$watchとは
$watchとは任意のモデルの変化を検知したときに、何か処理を行うときに使います。
例えば、三つの数の合計金額を表示するようなアプリケーションで三つの数を弄っているときに、数が変われば合計金額を変えないといけないようなときに使います。
$watchを使って数の変化を監視して、それが変化したときに合計金額が変更するように書けばOKです。
$apply、$digestの存在理由
前にも書いた通り、AngularJSでは
- DOMの変更→$scopeに反映
- $scopeの変更→DOMに反映
を自動でやってくれていて(モデルと$scopeがバインディングされていて)、特定のモデルの変化時に何か処理をしたいときには$watchを使えばいいのでした。
ただし、$watchを使ってもモデルの変化を検知できずに、反映されない場合がたまーにあります。
それは「AngularJSはAngularJSのイベントしか管理していない」からです。
さらに詳しく説明するために、$watchの仕組みについて考えてみます。
$watchでは、ある特定のイベントが起きたときに$digestループというものが実行され、そのループの中で$watchの処理が実行されます。
特定のイベントとは、具体的に以下です。
- ブラウザのロケーションの変更($location)
- ネットワーク系のイベント($http、$resourceなど)
- DOMイベント(ng−click、ng−mouse~、など)
- タイマー($timeout)
これらのイベントはAngularJSの管理下にあるイベントです。
()の中に書いているのは、AngularJS内でこれらのイベントを起こすための方法です。
つまり、AngularJSの管理下にないイベントを引き起こしても、AngularJSではそれらのイベントを検知できないということです。
例えば、window.locationを使えばロケーションの移動ができますが、このやり方でイベントを起こしてもAngularJSでイベントを検知できないので、AngularJSでラップされた$locationを使う必要があります。
タイマーについても、setTimeoutを使ってもAngularJS中ではうんともすんとも言わなので$timeoutを使わなければなりません。
その他のイベントについても同様です。
というわけで、AngularJSでは起こすことのできないイベントを、サードパーティのライブラリを使って起こしたり、自作の関数で起こしたりして、モデル($scope)を変更した場合には、プログラマの意思で$digestループを実行しないといけないのです。
その手段が、$apply、$digestなのです。
$digestのやっていること
$digestでは、$watchに登録された処理を、変化がなくなるまで実行し続けます。
$applyのやっていること
$digestループを引き起こします。
ちなみに、AngularJSのイベントでは内部的に$applyが呼ばれているそうです。
パフォーマンスの問題
注意すべきところがあって、$digestは登録された処理の結果が変わらなくなるまで、ずっと繰り返し続けてしまいます。
$watchに100個登録されていたとして、そのうち一つの変更を検知してもとりあえず100個の処理が実行されてしまうのでパフォーマンスの低下には要注意です。
まとめ
- $watch: モデルの変化にひもづき、処理を実行する。$digestループを実行することで実現。
- $digest: $watchに登録された処理を、変化がなくなるまで繰り返す
- $apply: $digestループを実行する。AngularJS以外のイベントを利用したい場合に使う。