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


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.

La seule façon dont je sais de faire cela est d'appeler $apply() de la portée de mes contrôleurs et 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?


784
2017-10-04 14:07


origine


Réponses:


Ne pas utiliser ce motif - 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 un $digest est déjà en cours en vérifiant $scope.$$phase.

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

$scope.$$phase reviendra "$digest" ou "$apply" si un $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 de ses enfants, et $apply traitera les observateurs de tous les champs d'application.

Pour le point @ dnc253, si vous vous trouvez appelant $digest ou $apply fréquemment, vous pouvez le faire mal. 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 fais pas if (!$scope.$$phase) $scope.$apply(), cela signifie que $scope.$apply() n'est pas assez élevé dans la pile d'appels.

635
2017-10-12 12:28



D'une discussion récente avec les gars d'Angular sur ce sujet: Pour des raisons d'avenir, vous ne devez 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 0 par défaut s'il n'est pas défini ($ timeout appels $ browser.defer lequel 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!


631
2017-09-25 04:06



Le cycle de digestion est un appel synchrone. Il ne donnera pas de contrôle à la boucle d'événement du navigateur jusqu'à ce qu'il soit fait. Il y a plusieurs façons de gérer cela. La façon la plus simple de gérer cela est d'utiliser le $ timeout intégré, et une seconde façon est d'utiliser underscore ou lodash (et vous devriez l'être), appelez ceci:

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

ou si vous avez un trait de soulignement:

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

Nous avons essayé plusieurs solutions de contournement, et nous détestions injecter $ rootScope dans tous nos contrôleurs, directives et même dans certaines usines. Donc, le $ timeout et le _.defer ont été nos préférés jusqu'ici. Ces méthodes indiquent avec succès à angular d'attendre la prochaine boucle d'animation, ce qui garantira que la portée actuelle. $ Apply est terminée.


312
2017-07-30 22:51



La plupart des réponses contiennent de bons conseils mais peuvent aussi mener à la confusion. Simplement en utilisant $timeout est ne pas la meilleure et 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é 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écute le rappel, puis exécute à la fin un cycle complet $apply.

  • $timeout(callback, delay, false) fera de même (avec un délai optionnel avant d'exécuter le rappel), mais ne déclenchera pas un $apply (troisième argument) qui enregistre les performances si vous n'avez pas modifié votre modèle angulaire ($ scope).

  • $scope.$apply(callback) invoque, entre autres, $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 à 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 le $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 être en charge de cela (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 sauvegarder les 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 (depuis une nouvelle $apply sera déclenché si aucun n'est en cours), mais c'est la meilleure solution lorsque vous exécutez une fonction qui vous ne pouvez pas le savoir si sera exécuté 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 un !$scope.$$phase, assurez-vous d'utiliser $scope.$evalAsync( callback )


257
2018-04-16 06:59



Petite méthode pratique pour garder ce processus au sec:

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

86
2018-06-14 18:14



Voir http://docs.angularjs.org/error/$rootScope:inprog

Le problème se pose lorsque vous avez un appel à $apply qui est parfois exécuté de manière asynchrone en dehors du code angulaire (quand $ apply doit être utilisé) et parfois de manière synchrone dans le code angulaire (qui provoque la $digest already in progressErreur).

Cela peut se produire, par exemple, lorsque vous avez une bibliothèque qui récupère de façon asynchrone des éléments d'un serveur et les met en cache. La première fois qu'un élément est demandé, il sera récupéré de manière asynchrone afin de ne pas bloquer l'exécution du code. La deuxième fois, cependant, l'élément est déjà dans le cache afin qu'il puisse être récupéré de manière synchrone.

La façon d'éviter cette erreur est de s'assurer que le code qui appelle $apply est exécuté de manière asynchrone. Cela peut être fait en exécutant votre code dans un appel à $timeout avec le délai fixé à 0 (qui est la valeur par défaut). Cependant, en appelant votre code à l'intérieur $timeout supprime la nécessité d'appeler $apply, car $ timeout déclenchera un autre $digest cycle seul, qui, à son tour, fera toute la mise à jour nécessaire, etc.

Solution

En bref, au lieu de faire ceci:

... your controller code...

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

... more of your controller code...

fais ceci:

... your controller code...

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

... more of your controller code...

Appelez seulement $apply Lorsque vous savez que le code est en cours d'exécution, il sera toujours exécuté en dehors du code Angular (par exemple, votre appel à $ apply se produira dans un rappel qui est appelé par le code en dehors de votre code Angular).

À moins que quelqu'un ne soit conscient d'un désavantage pernicieux $timeout plus de $apply, Je ne vois pas pourquoi tu ne pourrais pas toujours utiliser $timeout (sans délai) au lieu de $apply, car il fera à peu près la même chose.


31
2018-01-21 21:33



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

  }]
)

31
2017-09-03 00:15



Lorsque vous obtenez cette erreur, cela signifie qu'il est déjà en train de mettre à jour votre vue. Vous ne devriez vraiment pas 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 correctement le modèle. Si vous postez quelques détails, nous pourrions déterminer le problème principal.


28
2017-10-04 14:41



La forme la plus courte de coffre-fort $apply est:

$timeout(angular.noop)

14
2017-11-13 13:00