web-dev-qa-db-ja.com

AngularJS:$ scopeを呼び出すときにエラー$ digestがすでに進行中であることを防ぎます。

私は自分のページに自分のページを手動で更新する必要があることに気づいています。

これを行うために私が知っている唯一の方法は、私のコントローラーとディレクティブの範囲から$apply()を呼び出すことです。これに関する問題はそれが読むコンソールにエラーを投げ続けることです:

エラー:$ digestは既に進行中です

誰かがこのエラーを回避する方法や同じことを別の方法で達成する方法を知っていますか?

797
Lightbulb1

このパターンを使わないでください - これは解決するよりも多くのエラーを引き起こすことになります。あなたはそれが何かを直したと思いますが、それはしませんでした。 

$digestをチェックすることによって、$scope.$$phaseがすでに進行中であるかどうかをチェックすることができます。 

if(!$scope.$$phase) {
  //$digest or $apply
}

$scope.$$phaseまたは"$digest"が進行中の場合、"$apply"$digestまたは$applyを返します。これらの状態の違いは、$digestが現在のスコープとその子のウォッチを処理し、$applyがすべてのスコープのウォッチャーを処理するということです。

@ dnc253にとっては、$digestまたは$applyを頻繁に呼び出していることに気付いた場合は、間違っている可能性があります。一般的に、Angularの手が届かないところでDOMイベントが発生したためにスコープの状態を更新する必要がある場合は、ダイジェストが必要です。たとえば、Twitterのブートストラップモーダルが非表示になったときなどです。 $digestが進行中のときにDOMイベントが発生することがあります。だから私はこのチェックを使うのです。 

誰かがそれを知っていれば私はもっと良い方法を知りたいのです。


コメントから: by @anddoutoi

angular.jsアンチパターン

  1. if (!$scope.$$phase) $scope.$apply()をしないでください、それはあなたの$scope.$apply()がコールスタックで十分に高くないことを意味します。
645
Lee

この非常にトピックに関するAngularの人たちとの最近の議論から:将来を保証するために、$$phase

それを行うための「正しい」方法を押すと、答えは現在

$timeout(function() {
  // anything you want can go here and will safely be run on the next digest.
})

私は最近、さまざまな程度でコールバックが渡されるfacebook、google、Twitter APIをラップするangularサービスを作成するときにこれに遭遇しました。

これはサービス内からの例です。 (簡潔にするために、変数の設定、$ timeoutの挿入などのサービスの残りは省略されています。)

window.gapi.client.load('oauth2', 'v2', function() {
    var request = window.gapi.client.oauth2.userinfo.get();
    request.execute(function(response) {
        // This happens outside of angular land, so wrap it in a timeout 
        // with an implied apply and blammo, we're in action.
        $timeout(function() {
            if(typeof(response['error']) !== 'undefined'){
                // If the google api sent us an error, reject the promise.
                deferred.reject(response);
            }else{
                // Resolve the promise with the whole response if ok.
                deferred.resolve(response);
            }
        });
    });
});

$ timeoutの遅延引数はオプションであり、設定されていない場合はデフォルトで0になります( $ timeout 呼び出し $ browser.defer which 遅延の場合はデフォルトで0になります)設定されていません

少し直感的ではありませんが、それはAngularを書いている人からの答えですので、私にとっては十分です!

657
betaorbust

ダイジェストサイクルは同期呼び出しです。それが行われるまで、それはブラウザのイベントループに制御を譲りません。これに対処する方法がいくつかあります。これに対処する最も簡単な方法は、組み込みの$ timeoutを使用することです。2番目の方法は、アンダースコアまたはlodashを使用している場合(そして使用する必要がある場合)は、次のように呼び出します。

$timeout(function(){
    //any code in here will automatically have an apply run afterwards
});

またはあなたがアンダースコアを持っているならば:

_.defer(function(){$scope.$apply();});

いくつかの回避策を試しましたが、すべてのコントローラ、ディレクティブ、さらにはいくつかの工場にも$ rootScopeを注入するのは嫌でした。そのため、$ timeoutと_.deferがこれまでのところ私たちのお気に入りでした。これらのメソッドは、次のアニメーションループまで待つようにangleに指示することに成功し、それは現在のスコープが適用されることを保証します。 

318
frosty

ここでの回答の多くには良いアドバイスが含まれていますが、混乱を招く可能性もあります。単純に$timeoutを使用することはnotであり、最良のソリューションでもありません。また、パフォーマンスやスケーラビリティに懸念がある場合は、必ずお読みください。

知っておくべきこと

  • $$phaseはフレームワークに対してプライベートであり、それには十分な理由があります。

  • $timeout(callback)は、現在のダイジェストサイクル(存在する場合)が完了するまで待機してからコールバックを実行し、最後に$apply全体を実行します。

  • $timeout(callback, delay, false)は同じことを行います(コールバックを実行する前にオプションの遅延があります)が、Angularを変更しなかった場合にパフォーマンスを保存する$apply(3番目の引数)を起動しません_モデル($ scope)。

  • $scope.$apply(callback)は、とりわけ$rootScope.$digestを呼び出します。つまり、孤立したスコープ内にいる場合でも、アプリケーションとそのすべての子のルートスコープを再消化します。

  • $scope.$digest()は単純にモデルをビューに同期しますが、親スコープを消化しません。これにより、HTMLの分離された部分で(主にディレクティブから)作業する際のパフォーマンスを大幅に節約できます。 $ digestはコールバックを行いません。コードを実行してからダイジェストします。

  • $scope.$evalAsync(callback)はanglejs 1.2で導入されたもので、おそらくあなたの問題のほとんどを解決するでしょう。詳細については、最後の段落を参照してください。

  • $digest already in progress errorを取得した場合、アーキテクチャは間違っています。スコープを再ダイジェストする必要がないか、またはそれを担当するべきではありません(以下を参照)。

コードを構成する方法

そのエラーが発生した場合、既に進行中のスコープをダイジェストしようとしています。その時点でスコープの状態がわからないため、そのダイジェストの処理は担当していません。

function editModel() {
  $scope.someVar = someVal;
  /* Do not apply your scope here since we don't know if that
     function is called synchronously from Angular or from an
     asynchronous code */
}

// Processed by Angular, for instance called by a ng-click directive
$scope.applyModelSynchronously = function() {
  // No need to digest
  editModel();
}

// Any kind of asynchronous code, for instance a server request
callServer(function() {
  /* That code is not watched nor digested by Angular, thus we
     can safely $apply it */
  $scope.$apply(editModel);
});

そして、大きなAngularアプリケーションの一部であるときに孤立した小さなディレクティブで何をして作業しているのかがわかっている場合、パフォーマンスを節約するために$ applyよりも$ digestを好むかもしれません。

Angularjs 1.2以降の更新

新しい強力なメソッドが$ scopeに追加されました:$evalAsync。基本的に、現在のダイジェストサイクルが発生している場合は、現在のダイジェストサイクル内でコールバックを実行します。そうでない場合は、新しいダイジェストサイクルがコールバックの実行を開始します。

HTMLの分離された部分のみを同期する必要があることが本当にわかっている場合は、$scope.$digestほど良くありません(新しい$applyは何も進行していない場合にトリガーされるため) 同期的に実行されるかどうかがわからない、たとえば、潜在的にキャッシュされているリソースを取得した後の関数を実行する場合の最適なソリューションです:サーバーへの非同期呼び出しが必要になる場合がありますそうでない場合、リソースはローカルで同期的にフェッチされます。

これらの場合、および!$scope.$$phaseを持っている他のすべての場合は、必ず$scope.$evalAsync( callback )を使用してください

265
floribon

このプロセスをDRYに保つための便利な小さなヘルパーメソッド: 

function safeApply(scope, fn) {
    (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}
86
lambinator

http://docs.angularjs.org/error/$rootScope:inprog を参照してください

この問題は、Angularコードの外側で非同期に実行される$applyの呼び出し($ applyを使用する必要がある場合)およびAngularコード内で同期的に実行される($digest already in progressエラーの原因)ときに発生します。

これは、たとえば、サーバーから非同期的にアイテムを取得してキャッシュするライブラリがある場合に発生する可能性があります。アイテムが最初に要求されると、コードの実行をブロックしないように非同期で取得されます。ただし、2回目はアイテムが既にキャッシュにあるため、同期的に取得できます。

このエラーを防ぐ方法は、$applyを呼び出すコードが非同期で実行されるようにすることです。これは、遅延を$timeout(デフォルト)に設定して、0の呼び出し内でコードを実行することで実行できます。ただし、$timeout内でコードを呼び出すと、$applyを呼び出す必要がなくなります。これは、$ timeoutがそれ自体で別の$digestサイクルをトリガーし、必要な更新などをすべて実行するためです。

ソリューション

要するに、これを行う代わりに:

... your controller code...

$http.get('some/url', function(data){
    $scope.$apply(function(){
        $scope.mydate = data.mydata;
    });
});

... more of your controller code...

これを行う:

... your controller code...

$http.get('some/url', function(data){
    $timeout(function(){
        $scope.mydate = data.mydata;
    });
});

... more of your controller code...

実行中のコードが常にAngularコードの外側で実行されることがわかっている場合にのみ$applyを呼び出します(たとえば、$ applyへの呼び出しは、Angularコードの外側のコードによって呼び出されるコールバック内で発生します)。

誰かが$timeoutよりも$applyを使用することの衝撃的な欠点に気付いていない限り、ほぼ同じことを行うため、$timeoutの代わりに$apply(ゼロ遅延)を常に使用できない理由はわかりません。

32
Trevor

たとえばCodeMirrorやKrpanoなどのサードパーティ製スクリプトでも同じ問題が発生しました。

しかし、$ timeoutサービスを使用することで解決しました(最初に挿入することを忘れないでください)。

したがって、次のようになります。

$timeout(function() {
  // run my code safely here
})

そしてあなたのコードの中であなたが使っているなら 

この

おそらく、それがファクトリディレクティブのコントローラの内部にあるか、単に何らかの種類のバインディングを必要としているためです。

.factory('myClass', [
  '$timeout',
  function($timeout) {

    var myClass = function() {};

    myClass.prototype.surprise = function() {
      // Do something suprising! :D
    };

    myClass.prototype.beAmazing = function() {
      // Here 'this' referes to the current instance of myClass

      $timeout(angular.bind(this, function() {
          // Run my code safely here and this is not undefined but
          // the same as outside of this anonymous function
          this.surprise();
       }));
    }

    return new myClass();

  }]
)
31
Ciul

このエラーが発生した場合、基本的にはビューの更新中です。あなたは本当にあなたのコントローラの中で$apply()を呼び出す必要はないはずです。あなたのビューが期待通りに更新されず、そして$apply()を呼び出した後にこのエラーを受け取る場合、それはおそらくあなたがモデルを正しく更新していないということです。あなたがいくつかの詳細を投稿した場合、私たちはコア問題を解明することができます。

28
dnc253

安全な$applyの最短形式は次のとおりです。

$timeout(angular.noop)
14
Warlock

EvalAsyncを使うこともできます。ダイジェストが終了した後に実行されます。

scope.evalAsync(function(scope){
    //use the scope...
});
11
CMCDragonkai

この方法を使用すると、エラーが発生することがあります( https://stackoverflow.com/a/12859093/801426 )。

これを試して:

if(! $rootScope.$root.$$phase) {
...
9
bullgare

まず第一に、このように修正しないでください

if ( ! $scope.$$phase) { 
  $scope.$apply(); 
}

$ phaseは$ダイジェストサイクルの単なるブール値のフラグなので、意味がありません。そのため、$ apply()が実行されないことがあります。それは悪い習慣だということを忘れないでください。

代わりに$timeoutを使用してください。

    $timeout(function(){ 
  // Any code in here will automatically have an $scope.apply() run afterwards 
$scope.myvar = newValue; 
  // And it just works! 
});

あなたが下線またはlodashを使用している場合は、defer()を使用できます。

_.defer(function(){ 
  $scope.$apply(); 
});
8
M Sagar

状況に応じて、$ evalAsyncまたは$ timeoutを使用してください。

これは良い説明の付いたリンクです。 

http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm

5
Luc

ダイジェストサイクルをトリガーするのではなく、カスタムイベントを使用することをお勧めします。

カスタムイベントをブロードキャストし、このイベントのリスナーを登録することは、ダイジェストサイクルにあるかどうかにかかわらず、発生させたいアクションをトリガーするための優れたソリューションであることがわかりました。 

カスタムイベントを作成すると、そのイベントを購読しているリスナーのみをトリガーし、scopeを呼び出した場合のようにスコープにバインドされているすべてのウォッチをトリガーしないため、コードの効率も向上します。

$scope.$on('customEventName', function (optionalCustomEventArguments) {
   //TODO: Respond to event
});


$scope.$broadcast('customEventName', optionalCustomEventArguments);
4
nelsonomuto

yearofmooは、再利用可能な$ safeApply関数を作成するのに素晴らしい仕事をしました。 

https://github.com/yearofmoo/AngularJS-Scope.SafeApply

使用法 :

//use by itself
$scope.$safeApply();

//tell it which scope to update
$scope.$safeApply($scope);
$scope.$safeApply($anotherScope);

//pass in an update function that gets called when the digest is going on...
$scope.$safeApply(function() {

});

//pass in both a scope and a function
$scope.$safeApply($anotherScope,function() {

});

//call it on the rootScope
$rootScope.$safeApply();
$rootScope.$safeApply($rootScope);
$rootScope.$safeApply($scope);
$rootScope.$safeApply($scope, fn);
$rootScope.$safeApply(fn);
3
RNobel

$eval関数が実行されることがわかっている場所で、$applyの代わりに$digestを呼び出すことで、この問題を解決できました。

docs によれば、$applyは基本的にこれを行います:

function $apply(expr) {
  try {
    return $eval(expr);
  } catch (e) {
    $exceptionHandler(e);
  } finally {
    $root.$digest();
  }
}

私の場合、ng-clickはスコープ内の変数を変更し、その変数の$ watchは$appliedでなければならない他の変数を変更します。この最後のステップにより、「ダイジェストはすでに進行中です」というエラーが発生します。

監視式内で$apply$evalに置き換えることにより、スコープ変数が期待どおりに更新されます。

したがって、が表示されますAngular内の他の変更のためにダイジェストが実行される場合は、$eval 'ingが必要ですする。

2
teleclimber

代わりに$scope.$$phase || $scope.$apply();を使用してください

2

使ってみてください 

$scope.applyAsync(function() {
    // your code
});

の代わりに

if(!$scope.$$phase) {
  //$digest or $apply
}

$ applyAsync後で適用するように$ applyの呼び出しをスケジュールします。これは、同じダイジェストで評価する必要がある複数の式をキューに入れるために使用できます。

注:$ダイジェスト内では、現在のスコープが$ rootScopeの場合にのみ$ applyAsync()がフラッシュします。つまり、子スコープに対して$ digestを呼び出しても、暗黙的に$ applyAsync()キューをフラッシュすることはありません。

例:

  $scope.$applyAsync(function () {
                if (!authService.authenticated) {
                    return;
                }

                if (vm.file !== null) {
                    loadService.setState(SignWizardStates.SIGN);
                } else {
                    loadService.setState(SignWizardStates.UPLOAD_FILE);
                }
            });

参考文献: 

1. Scope。$ applyAsync()とAngularJS 1.3のScope。$ evalAsync()

  1. AngularJsドキュメント
2
Eduardo Eljaiek

私はこの方法を使ってきましたが、それは完璧に動作するようです。これは単にサイクルが終了するまで待ってからapply()を起動します。必要な場所から関数apply(<your scope>)を呼び出すだけです。

function apply(scope) {
  if (!scope.$$phase && !scope.$root.$$phase) {
    scope.$apply();
    console.log("Scope Apply Done !!");
  } 
  else {
    console.log("Scheduling Apply after 200ms digest cycle already in progress");
    setTimeout(function() {
        apply(scope)
    }, 200);
  }
}
1
Ashu

Angularドキュメントが$$phaseおよび anti-pattern のチェックを呼び出すことを理解して、私は$timeout_.deferを動かそうとしました。

Timeoutメソッドとdeferredメソッドは、 _ fout _ のようにDOM内に解析されていない{{myVar}}コンテンツのフラッシュを作成します。私にとってこれは受け入れられませんでした。何かがハックであり、適切な代替手段を持っていないと独断的に言われることは多くのことを私に残します。

毎回動作するのは、唯一のことです。

if(scope.$$phase !== '$digest'){ scope.$digest() }

私はこの方法の危険性を理解していませんし、それがコメントやアンギュラチームの人々によってハックとして説明されているのはなぜですか。コマンドは正確で読みやすいようです。

「すでに起こっていない限り、ダイジェストを実行する」

CoffeeScriptでは、さらにきれいです。

scope.$digest() unless scope.$$phase is '$digest'

これの問題は何ですか? FOUTを作成しない代替手段はありますか? $ safeApply は問題ないように見えますが、$$phaseの検査方法も使用します。

1
SimplGy

これは私のutilsサービスです:

angular.module('myApp', []).service('Utils', function Utils($timeout) {
    var Super = this;

    this.doWhenReady = function(scope, callback, args) {
        if(!scope.$$phase) {
            if (args instanceof Array)
                callback.apply(scope, Array.prototype.slice.call(args))
            else
                callback();
        }
        else {
            $timeout(function() {
                Super.doWhenReady(scope, callback, args);
            }, 250);
        }
    };
});

これはその使用法の例です。

angular.module('myApp').controller('MyCtrl', function ($scope, Utils) {
    $scope.foo = function() {
        // some code here . . .
    };

    Utils.doWhenReady($scope, $scope.foo);

    $scope.fooWithParams = function(p1, p2) {
        // some code here . . .
    };

    Utils.doWhenReady($scope, $scope.fooWithParams, ['value1', 'value2']);
};
1
ranbuch

上記の回答と似ていますが、これは私にとって忠実に機能しました... サービスの追加:

    //sometimes you need to refresh scope, use this to prevent conflict
    this.applyAsNeeded = function (scope) {
        if (!scope.$$phase) {
            scope.$apply();
        }
    };
0
Shawn Dotey

あなたが使用することができます 

$timeout

エラーを防ぐために。 

 $timeout(function () {
                        var scope = angular.element($("#myController")).scope();
                        scope.myMethod();
                        scope.$scope();
                    },1);
0
Satish Singh