angularjs - error:[$rootscope:inprog] $digest already in progress




AngularJS: empêche l'erreur $ digest déjà en cours lors de l'appel de $ scope. $ Apply() (17)

N'utilisez pas ce modèle - Cela finira par causer plus d'erreurs qu'il n'en résout. Même si vous pensez que cela a arrangé quelque chose, ce n'est pas le cas.

Vous pouvez vérifier si $digest est déjà en cours en vérifiant $scope.$$phase .

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

$scope.$$phase retournera "$digest" ou "$apply" si $digest ou $apply est en cours. Je crois que la différence entre ces états est que $digest traitera les montres de la portée actuelle et ses enfants, et $apply traitera les observateurs de toutes les portées.

Pour le point de @ dnc253, si vous vous appelez $digest ou $apply fréquemment, vous pouvez vous tromper. Je trouve généralement que j'ai besoin de digérer quand j'ai besoin de mettre à jour l'état de la lunette à la suite d'un tir DOM hors de portée d'Angular. Par exemple, quand un modal bootstrap Twitter devient caché. Parfois, l'événement DOM se déclenche lorsqu'un $digest est en cours, parfois non. C'est pourquoi j'utilise cette vérification.

J'aimerais savoir un meilleur moyen si quelqu'un en connaît un.

De commentaires: par @anddoutoi

angular.js Anti Patterns

  1. Ne faites pas if (!$scope.$$phase) $scope.$apply() , cela signifie que votre $scope.$apply() n'est pas assez élevé dans la pile des appels.

Je trouve que j'ai besoin de mettre à jour ma page manuellement de plus en plus depuis la construction d'une application en mode angulaire.

Le seul moyen que je connaisse pour faire cela est d'appeler $apply() de la portée de mes contrôleurs et de mes directives. Le problème avec ceci est qu'il continue à jeter une erreur à la console qui lit:

Erreur: $ digest déjà en cours

Est-ce que quelqu'un sait comment éviter cette erreur ou réaliser la même chose mais d'une manière différente?


C'est mon service d'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);
        }
    };
});

et ceci est un exemple pour son utilisation:

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']);
};

Comprenant que les documents Angular appelaient la vérification de la $$phase un anti-pattern , j'ai essayé d'obtenir $timeout et _.defer pour fonctionner.

Les méthodes timeout et différées créent un flash de contenu {{myVar}} FOUT dans le dom comme un FOUT . Pour moi, ce n'était pas acceptable. Il me laisse peu de choses à dire dogmatiquement que quelque chose est un hack, et ne pas avoir une alternative convenable.

La seule chose qui fonctionne à chaque fois est:

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

Je ne comprends pas le danger de cette méthode, ou pourquoi il est décrit comme un hack par les gens dans les commentaires et l'équipe angulaire. La commande semble précise et facile à lire:

"Faites le résumé à moins que l'on ne se passe déjà"

Dans CoffeeScript, c'est encore plus joli:

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

Quel est le problème avec ça? Y a-t-il une alternative qui ne créera pas un FOUT? https://github.com/yearofmoo/AngularJS-Scope.SafeApply semble très bien mais utilise également la méthode d'inspection $$phase .


D'une discussion récente avec les gars d'Angular sur ce sujet: Pour des raisons d'avenir, vous ne devriez pas utiliser $$phase

Lorsque pressé pour le "bon" moyen de le faire, la réponse est actuellement

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

Je me suis récemment heurté à cela en écrivant des services angulaires pour envelopper les API facebook, google et twitter qui, à des degrés divers, ont des rappels remis.

Voici un exemple tiré d'un service. (Par souci de brièveté, le reste du service - qui a mis en place des variables, injecté $ timeout, etc. - a été arrêté.)

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);
            }
        });
    });
});

Notez que l'argument de délai pour $ timeout est facultatif et sera par défaut à 0 si laissé non défini ( $timeout appelle $browser.defer qui est par défaut à 0 si le délai n'est pas défini )

Un peu non-intuitif, mais c'est la réponse des gars qui écrivent Angular, donc c'est assez bon pour moi!


J'ai eu le même problème avec des scripts tiers comme CodeMirror par exemple et Krpano, et même en utilisant les méthodes safeApply mentionnées ici n'ont pas résolu l'erreur pour moi.

Mais qu'est-ce qui l'a résolu est l'utilisation du service $ timeout (n'oubliez pas de l'injecter en premier).

Ainsi, quelque chose comme:

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

et si dans votre code vous utilisez

ce

peut-être parce que c'est dans le contrôleur d'une directive d'usine ou juste besoin d'une sorte de liaison, alors vous feriez quelque chose comme:

.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();

  }]
)

J'ai utilisé cette méthode et il semble fonctionner parfaitement bien. Cela attend juste que le cycle soit terminé et que les déclencheurs apply() . Appelez simplement la fonction apply(<your scope>) de n'importe où vous voulez.

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);
  }
}

La forme la plus courte de safe $apply est:

$timeout(angular.noop)

La plupart des réponses contiennent de bons conseils mais peuvent aussi mener à la confusion. L'utilisation de $timeout n'est pas la meilleure ni la bonne solution. Aussi, assurez-vous de lire cela si vous êtes concerné par les performances ou l'évolutivité.

Ce que vous devriez savoir

  • $$phase est privée au cadre et il y a de bonnes raisons à cela.

  • $timeout(callback) attendra que le cycle de digestion actuel (le cas échéant) soit terminé, puis exécutera le rappel, puis exécutera à la fin un $apply complet.

  • $timeout(callback, delay, false) fera de même (avec un délai optionnel avant l'exécution du callback), mais ne lancera pas $apply (troisième argument) qui sauvegarde les performances si vous n'avez pas modifié votre modèle Angular ($ scope ).

  • $scope.$apply(callback) invoque, entre autres choses, $rootScope.$digest , ce qui signifie qu'il va redessiner la portée racine de l'application et tous ses enfants, même si vous êtes dans une portée isolée.

  • $scope.$digest() synchronisera simplement son modèle avec la vue, mais ne digérera pas la portée de ses parents, ce qui peut sauver beaucoup de performances lorsque vous travaillez sur une partie isolée de votre HTML avec une portée isolée (à partir d'une directive principalement) . $ digest ne prend pas de rappel: vous exécutez le code, puis digérez.

  • $scope.$evalAsync(callback) a été introduit avec angularjs 1.2, et résoudra probablement la plupart de vos problèmes. S'il vous plaît se référer au dernier paragraphe pour en savoir plus à ce sujet.

  • Si vous obtenez l' $digest already in progress error , alors votre architecture est fausse: soit vous n'avez pas besoin de redimensionner votre portée, soit vous ne devriez pas en être responsable (voir ci-dessous).

Comment structurer votre code

Lorsque vous obtenez cette erreur, vous essayez de digérer votre portée alors qu'elle est déjà en cours: puisque vous ne connaissez pas l'état de votre portée à ce moment-là, vous n'êtes pas responsable de gérer sa digestion.

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);
});

Et si vous savez ce que vous faites et travaillez sur une petite directive isolée tout en faisant partie d'une grande application angulaire, vous pourriez préférer $ digest au lieu de $ apply pour enregistrer des performances.

Mise à jour depuis Angularjs 1.2

Une nouvelle méthode puissante a été ajoutée à toute $ $ scope: $evalAsync . Fondamentalement, il va exécuter son callback dans le cycle de digestion en cours s'il y en a un, sinon un nouveau cycle de digest commencera à exécuter le callback.

Ce n'est toujours pas aussi bon qu'un $scope.$digest si vous savez vraiment que vous avez seulement besoin de synchroniser une partie isolée de votre HTML (puisqu'une nouvelle $apply sera déclenchée si aucune n'est en cours), mais c'est le meilleur solution lorsque vous exécutez une fonction que vous ne pouvez pas savoir si elle sera exécutée de manière synchrone ou non , par exemple après avoir récupéré une ressource potentiellement mise en cache: cela nécessitera parfois un appel asynchrone à un serveur, sinon la ressource sera récupérée localement de manière synchrone.

Dans ces cas et tous les autres où vous avez eu une !$scope.$$phase , assurez-vous d'utiliser $scope.$evalAsync( callback )


Lorsque vous obtenez cette erreur, cela signifie qu'il est déjà en train de mettre à jour votre vue. Vous ne devriez pas avoir besoin d'appeler $apply() dans votre contrôleur. Si votre vue n'est pas mise à jour comme prévu, et que vous obtenez cette erreur après avoir appelé $apply() , cela signifie probablement que vous ne mettez pas à jour le modèle correctement. Si vous postez quelques détails, nous pourrions déterminer le problème principal.


Parfois, vous aurez toujours des erreurs si vous utilisez de cette façon ( https://.com/a/12859093/801426 ).

Essaye ça:

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

Tout d'abord, ne le répare pas de cette façon

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

Cela n'a pas de sens car $ phase est juste un drapeau booléen pour le cycle $ digest, ainsi votre $ apply () ne fonctionnera pas. Et rappelez-vous que c'est une mauvaise pratique.

Au lieu de cela, utilisez $timeout

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

Si vous utilisez underscore ou lodash, vous pouvez utiliser defer ():

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

Trouvé ceci: https://coderwall.com/p/ngisma où Nathan Walker (près du bas de la page) suggère un décorateur dans $ rootScope pour créer func 'safeApply', code:

yourAwesomeModule.config([
  '$provide', function($provide) {
    return $provide.decorator('$rootScope', [
      '$delegate', function($delegate) {
        $delegate.safeApply = function(fn) {
          var phase = $delegate.$$phase;
          if (phase === "$apply" || phase === "$digest") {
            if (fn && typeof fn === 'function') {
              fn();
            }
          } else {
            $delegate.$apply(fn);
          }
        };
        return $delegate;
      }
    ]);
  }
]);


Vous pouvez également utiliser evalAsync. Il fonctionnera quelque temps après la fin du résumé!

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

essayez d'utiliser

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

au lieu de

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

$ applyAsync Planifie l'invocation de $ apply pour se produire ultérieurement. Cela peut être utilisé pour mettre en file d'attente plusieurs expressions qui doivent être évaluées dans le même résumé.

NOTE: Dans le $ digest, $ applyAsync () ne videra que si la portée actuelle est $ rootScope. Cela signifie que si vous appelez $ digest sur une portée enfant, elle ne videra pas implicitement la file d'attente $ applyAsync ().

Exmaple:

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

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

Les références:

1. Portée. $ ApplyAsync () vs Scope. $ EvalAsync () dans AngularJS 1.3

  1. AngularJs Docs

similaire aux réponses ci-dessus mais cela a fonctionné fidèlement pour moi ... dans un service ajouter:

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

yearofmoo a fait un excellent travail en créant une fonction $ safeApply réutilisable pour nous:

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

Utilisation:

//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);




angular-digest