Angular で「Error: 10 $digest() iterations reached. Aborting!」というエラーが出る時の対処法
最近 Angular で調べたことを週末に記事にして上げるサイクルになりつつある。良い傾向。
あと説明する時につっかからずに説明できたりするので、自分で調べてまとめて人に教える重要さをひしひしと感じる… なので最近は「書かなくても分かるやろ」ってとこを手抜きせず、あえて言うなら冗長に書いたりしてる。
本題
Angular は監視(バインド)している変数の値が変更された場合*1、他の値が変わるか毎回ループを回してチェックする。これを digest loop と呼びます。
冒頭のエラーメッセージは digest loop を10回回したけど毎回値が変わっていて収束しなさそうだし、キリがないのでやめますという意味 (ざっくり)。
こんな場合に出る
- ランダム値を返している場合
- インスタンスが毎回違う場合
対処法
いくつかあります。
- 関数ではなく変数にして $watch() により変更を伝播させる。
- 違うインスタンスを返している場合は計算した結果を変数に保存してキャッシュしておき、キャッシュがあり再計算の必要がない場合はそれを返すなどして、同じインスタンスを返すようにする (バッドプラクティスなのでやるべきではない)。
- One-time binding を使う
良くある一例として、ng-repeat の in ...
の部分では関数を使うべきではありません (1 のリンクを参照)。
もちろん関数で前述したようにキャッシュして常に同じインスタンスを返すようにすればエラーは起こらなくなるが、関数の中で無理矢理キャッシュして解決するのではなく、計算(関数)の終わりに計算結果を変数(モデル)にセットして、Angular 側に再計算させ変更を伝播させるようにすべきです。
つまり関数はキャッシュなどの不必要な副作用を持つべきではありません。 不必要というのは、変更を伝播させるために必ず計算結果を変数(モデル)にセットする必要があるからです。
キャッシュを使ってもべき等性(何回実行しても同じ結果が返ってくること)が保たれるなら問題ありません。 しかしバグを引き起こしやすくなるのであまり使うべきではないです。 Angular アプリのパフォーマンスを改善したければ Chrome の Angular 用拡張機能を使ってボトルネックを見つけ出す方が正道です。
しかしランダム値を返すなど、どうしてもべき等性を仕様的に担保できない場合もあります。 そういう場合は3番の One-time binding が有効です。
要点を箇条書きにすると以下の通りです。
- 関数はべき等にする
- 変更の伝播は Angular にやらせる
- 関数がランダム値を返すなどしてべき等性を保てないなら上で挙げた対処法を試す
所感
React 使ってる人からしたら結論は当たり前だと思うだろうし Angular 1 こわいと思われそう… (だと思ってるけど実際どうなんだろう)
実際自分も Angular 1 の digest loop は度々悩まされてるので、digest loop の無いフレームワーク使えたらなぁと思う。 というかそもそも digest loop が双方向バインディングを実現する(ちょっと無理矢理な)方法なので、一方向にして Flux アーキテクチャを導入できたらとは思う。
ただ、React だけだと Flux の強制力はないので、またさらに他のフレームワークが必要となって、そうすると学習コストとかで敬遠される確率が高まる (実際は Angular のが面倒難しくても)。
あとフルスタックな技術は検索しやすい。
そういう意味でフルスタックという点では Java と同じく多いに助かってます。 業務寄りではないけど業務向けな感じ*2のフレームワークという印象。
参考リンク
- ポケモンのタイプ相性を確認できるウェブアプリケーションを作りました - 詩と創作・思索のひろば
- $watch() を使う場合のサンプルコード
- javascript - Angular - Error: 10 $digest() iterations reached. Aborting - Stack Overflow
- 関数で毎回ランダムな値を返しているせいで digest loop が終わらないケース
- angularjs - angular Infinite $digest Loop in ng-repeat - Stack Overflow
- AngularJSのOne-time Bindingを使ってパフォーマンス改善をしよう | 株式会社LIG
- One-time binding の説明