Jim Hoskins氏の AngularJS and scope.$apply を日本語訳しました。JavaScriptの仕組みから解説されていて、とても分かりやすいです。
AngularJS and scope.$apply
もしあなたがAngularJSで多くの重要なコードを書いているなら、$scope.$apply()
メソッドを見たことがあるだろう。一見すると、バインドした変数を更新するためのメソッドのように見える。しかし、何故これが存在しているのか?そしていつこれを使うのか?
いつ$apply
を使うかを本気で理解するためには、 何故これを使う必要があるのかをきっちり知るのが良い。さあ、召し上がれ!
JavaScript is Turn Based
私たちが記述するJavaScriptのコードは一度に全て実行されるのではなく、ターンベースで実行される。各ターンは始めから終わりまで中断せずに走り、ターンが走っている間はブラウザ上では何も起きない。他のどのJavaScriptのコードも走っていない時は、Webページインタフェースは完全に固まる。だから不十分なJavaScriptコードはウェブページの動きを止めてしまう。
そうではなく、ある程度時間のかかるタスク、例えばAjaxリクエストやクリックイベントの待機、タイムアウトの設定のような場合、私たちはコールバック関数を設定して現在のターンを終える。その後、Ajaxリクエストが完了したり、クリックが検知されたり、タイマーが完了した場合に、新しいJavaScriptのターンが生成されて、コールバックが完了するまで走る。
次のJavaScriptファイルを見てみよう。
var button = document.getElementById('clickMe');
function buttonClicked () {
alert('the button was clicked');
}
button.addEventListener('click', buttonClicked);
function timerComplete () {
alert('timer complete');
}
setTimeout(timerComplete, 2000);
このJavaScriptコードが読み込まれた時にはシングルターンだ。これはボタンを探し、クリックリスナーを追加し、タイムアウトを設定する。そしてターンが完了した時、ブラウザは必要であればウェブページを更新して、ユーザーの入力を受け付ける。
もしブラウザが#clickMe
のクリックを検知したら、新しいターンを生成して、buttonClicked
関数を実行する。関数が戻ったら、ターンは完了する。
2000ミリ秒後、ブラウザはtimerComplete
と呼ばれる新しいターンを生成する。
JavaScriptコードは順繰りに実行され、ターンの間はページが再描画され、入力を受け付ける時間になる。
How do we update bindings?
Angularは私たちのインタフェースの部品をコード上のデータに縛ってやる。しかしデータが変更されてページの更新が必要であることをどうやって知るのだろうか。
いくつかの方法がある。コードは値が変更されたタイミングを知る必要がある。オブジェクトにはコードに直接変更を通知する方法はない。その代わりに2つの主な戦略がある。
ひとつは特別なオブジェクトを使うことだ。プロパティ代入ではなくメソッド経由でデータをセットする。変更した時に通知でき、ページを更新できる。これにはある特別なオブジェクトで拡張しなければならないという否定的側面がある。また、代入においては、より冗長な形式obj.set('key', 'value')
をobj.key = 'value'
の代わりに使わなければならない。EmberJS や KnockoutJS といったフレームワークではこの戦略が使われている。
AngularJSはまた別のアプローチ、どんな値でもバインドのターゲットにできるという方法を採用している。どのJavaScriptコードのターンが終わった時でも、値が変化したことを確認する。これは一見非効率に見えるかもしれないが、しかしいくつかのパフォーマンスヒットを下げる賢い戦略がある。大きな利点は、やりたいように普通のオブジェクトを使ったり、データを更新できて、そして変化がバインドしたものに通知されて反映されることにある。
この戦略を動かすためには、データが変更された可能性がある時点を知る必要がある。そしてこれが $scope.$apply
が動き始める場所だ。
$apply
and $digest
あらゆるバインドした値が実際に変化したことを確認するステップには、メソッド$scope.$digest()
がある。ここでは実際に魔法が起きており、私たちは決してそれを直接呼ぶことはなく、あなたに代わって$scope.$digest()
を呼んでくれる$scope.$apply()
を使う。
$scope.$apply()
は関数かAngular式の文字列を受け取り、それを実行する。そしてバインドやウォッチを更新するため$scope.$digest()
を呼ぶ。
それで、いつ$apply()
を呼ぶ必要があるのか?実際にはほとんどない。AngularJSは$apply呼び出しの内部であなたのコードの大部分を実行している。ng-click
のようなイベント、コントローラーの初期化、$http
によるコールバックは全て$scope.$apply()
で包まれている。だからあなたはそれを自分で呼ぶ必要はない、というよりできない。$applyの内部で$applyを呼ぶとエラーが投げられるからだ。
もし新しいターンでコードを走らせるつもりなら、あなたはこれを使う必要がある。そのターンがAngularJSライブラリのメソッドから生成されたのではない場合だけ。新しいターンの内部で、$scope.$apply()
であなたのコードを包む必要がある。例を示そう。この例ではsetTimeout
を使っているが、これは遅れて新しいターンで関数を実行するものだ。Angularはその新しいターンを知らないので、更新が反映されない。
しかし、ここで$scope.$apply()
でそのターンのコードをラップしたなら、変更は通知され、ページは更新される。
便利なことにAngularJSは$timeout を提供している。それはsetTimeout
のようだが、自動的に$applyであなたのコードをラップしている。それを使うと良い、これではなく。
もし$http
なしでAjaxを使ったコードを書いていたり、Angularのng-*
リスナーを使わずにイベントを設定しているなら、はたまた$timeout
を使わずにタイムアウトを設定しているなら、$scope.$apply
でコードをラップしなければならない。
$scope.$apply()
vs $scope.$apply(fn)
たまにデータが更新されたところで$scope.$apply()
が引数なしで呼ばれている、という例を見かける。これは求められた結果を実現するが、いくつかの場合に失敗する。
もしあなたのコードが$apply
関数でラップされておらずエラーを投げた場合、エラーはAngularJS外側に投げられる。これはあなたのアプリケーションで使われているあらゆるエラーハンドリングが扱えないということを意味する。$apply
はあなたのコードを実行するだけでなく、try/catch
でそれを走らせるのでいつでもエラーを捕まえられることができ、$digest
がfinally
節で呼ばれるため、エラーを気にせず実行できる。それはとても良いね。
あなたが$apply
が何であるかと、いつ使うべきかを理解できるといいね。もしAngularJSが提供するものだけを使うなら、あなたはこれを頻繁に使うべきではない。しかし、DOM要素を観察するようなディレクティブを書く場合には、これが必要になる。