こんにちは。
先日ng-japanにスポンサー企業として参加してきました、先生です。
本日は、すごくマジメに資料を作ったのに爆笑に包まれてしまったLTで発表した内容を、整理してお送りします。なぜ爆笑されたかについてはここでは語りませんので、直接聞くか動画をご覧ください。
当日のLT資料はこちらになります。
One-time Bindingとは
「バインディングを1度しか評価しないようにする機能」です。
通常のバインディングは値が変更されると画面の値も変更されますが、One-time Bindingを使うと描画された以降は再評価されなくなります。
サンプルで実際の動作を見てみましょう。
ソースは至ってシンプルです。
<div id="demo" ng-controller="RootCtrl">
<div>{{count}}</div><!-- いつものバインディング -->
<div>{{::count}}</div><!-- One-time Binding -->
</div>
angular.module('app').controller('RootCtrl',[
'$scope',
'$interval',
function($scope,$interval) {
$scope.count = 0;
$interval(function() {
$scope.count++;
},1000);
}
]);
1秒毎に$scope.countを増やしていくだけです。
通常のバインディングの方はcountが増える度に画面も変更されますが、One-time Bindingのほうは0のままになっています。
なぜこれが良いのか
単純にパフォーマンスの向上につながります。
その理由は、AngularJSが$scopeの値をチェックして画面に反映している機構にあります。それが$digestで行われているDirty Checkingというものです。
Dirty Checkingとは一定のタイミングで$scopeの値が変更されたかどうかを全てチェックする方式で、AngularJSの場合は$applyを起点としてチェックが行われるようになっています。
泥臭くチェックするからDirty Checkingなんですね。
demo1の例では$timeoutが実行された後に内部で$applyが呼び出されます。
$applyが呼び出されると、値が変更されていないかどうかを$digestでチェックしていきます。ここで$scopeに入っている値が前回から変わっていないかどうかをループしてチェックしていくので、$digest loopなどと呼ばれたりもします。
このチェックフローから、たとえ値を変更していなくても他の$scopeの値に変更があった場合には、値が変わっていないかどうかがチェックされます。
このチェックされる要素の数をWatch数と呼びます。
※ ざっくりしたフローの図
詳しい動作フローについては公式のドキュメントに記載があります。
https://code.angularjs.org/1.3.15/docs/guide/scope#integration-with-the-browser-event-loop
$digestの記述は以下の部分にありますので、どのようにチェックしているか興味がある方はソースを読んでみてください。
https://github.com/angular/angular.js/blob/master/src/ng/rootScope.js#L726-L836
One-time Bindingにすることで、このチェックを1度だけにすることができます。AngularJSのパフォーマンス改善のもっともシンプルな方法はこのWatch数を減らすことにあります。
Watch数の確認方法
そのWatch数がいくつなのかを確認する方法が必要になりますね。
とても簡単にチェックするためにGoogle Chromeの拡張を利用します。
https://chrome.google.com/webstore/detail/angular-watchers/nlmjblobloedpmkmmckeehnbfalnjnjk
Angular Watchersの使い方
Chromeウェブストアから上記の拡張機能をインストールします。
AngularJSが使われているサイトをChromeで表示してDevToolsを開きます。
Element, Networkと並んでいるタブに$$watchersという項目が増えているのでそれをクリックします。
これでDevTools内にWatch数が表示されます。非常に簡単にチェックができるのでオススメです。
さっそく先ほどのサンプルをAngular Watchersで見てみましょう。
すると1 diff: +1と表示されます。このサンプルのWatch数は1。つまり通常のバインディングをしてカウントアップしている部分だけとなります。
One-time Bindingを使ったWatch数の変化
続いてもう少し実践に近いサンプルでWatch数の変化を見ていきましょう。
以下のようなJSONファイルを用意しました。このような記述が500個続きます。
[
{
"id": 1,
"name": "User1",
"value": 100
},
...
]
JavaScriptはシンプルに以下のような感じにしました。
USERSには先ほどのJSONデータが入っています。
angular.module('app').controller('RootCtrl',[
'$scope',
'USERS',
function($scope,USERS) {
$scope.users = USERS;
}
]);
さっそくこのサンプルをAngular Watchersで見てみましょう。
One-time Bindingを使わない場合
まずは使わない場合のWatch数を見てみます。
<div ng-controller="RootCtrl">
<ul>
<li ng-repeat="user in users">
<div>{{user.id}} - {{user.name}}</div>
<div>{{user.value}}</div>
</li>
</ul>
</div>
Watch数: 1501
usersという配列+({{user.id}}+{{user.name}}+{{user.value}}) が500個で1501になっています。なかなかいい数ですね。一般に2000を超えると重いと感じると言われていますが、この時点でもやや負荷を感じます。
配列に付けた場合
One-time Bindingは配列にも付けることができます。
配列に付けた場合のWatch数を見てみます。
<div ng-controller="RootCtrl">
<ul>
<li ng-repeat="user in ::users">
<div>{{user.id}} - {{user.name}}</div>
<div>{{user.value}}</div>
</li>
</ul>
</div>
::usersとしてusersをOne-time Bindingしました。
Watch数: 1500
({{user.id}}+{{user.name}}+{{user.value}}) が500個で1500になっています。
配列分だけWatch数が減ったようです。
プロパティに付けた場合
userで参照しているプロパティ全部をOne-time Bindingにしてみます。
配列につけたものは一旦取り除きました。
<div ng-controller="RootCtrl">
<ul>
<li ng-repeat="user in users">
<div>{{::user.id}} - {{::user.name}}</div>
<div>{{::user.value}}</div>
</li>
</ul>
</div>
ループ内のプロパティにOne-time bindingした例
Watch数: 1
users配列だけになったので1になりました。かなり減りました。
全部に付けた場合
usersもプロパティも全部One-time Bindingにしてみました。
<div ng-controller="RootCtrl">
<ul>
<li ng-repeat="user in ::users">
<div>{{::user.id}} - {{::user.name}}</div>
<div>{{::user.value}}</div>
</li>
</ul>
</div>
Watch数: 0
ついにはWatch数が0になりました。この状態で$scopeの値を変更しても一切チェックされません。
まとめ
AngularJSでもっともシンプルにパフォーマンス改善をする方法は、Watch数を減らすことです。双方向のバインディングが必要のない部分では、適切にOne-time Bindingをしていきましょう。
つい忘れてしまうところだと思いますので、ときどきAngular Watchersを使ってWatch数を計測してパフォーマンス向上を心がけましょう。
【AngularJS活用術】
※ AngularJS勉強会 ng-mtg#6に登壇してきました
※ LIG主催のAngularJS勉強会 #ngCurryが開催されました
※ 業務で安心して使える厳選AngularJSモジュール8選+α
※ AngularJSで作成したシングルページアプリケーションをGoogleアナリティクスでトラッキングさせる方法
※ ECMAScript6で書こう!WebPackとES6-loaderで環境を作り、ES6を先取り体験する方法
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。