AngularJS: prévient l'erreur $ digest déjà en cours lors de l'appel de $scope.$appliquer()

je trouve que je dois mettre à jour ma page à ma portée manuellement de plus en plus depuis la construction d'une application en angle.

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

erreur: $ digest déjà en cours

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

789
demandé sur Ricky Dam 2012-10-04 18:07:32

25 réponses

N'utilisez pas ce modèle - cela finira par causer plus d'erreurs qu'il ne résout. Même si vous pensez qu'il a réparé quelque chose, il ne l'a pas fait.

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

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

$scope.$$phase retournera "$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 toutes les portées.

au point de @dnc253, si vous vous retrouvez à appeler $digest ou $apply fréquemment, vous pourriez 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 portée à la suite d'un événement DOM tir en dehors de la portée de L'angle. Par exemple, quand un bootstrap modal twitter devient cachée. Parfois L'événement DOM se déclenche quand un $digest est en cours, parfois non. C'est pour ça que j'utilise ce chèque.

j'aimerais savoir une meilleure façon, si quelqu'un connaît un.


des commentaires: par @anddoutoi

angulaire.js anti Patterns

  1. ne faites pas if (!$scope.$$phase) $scope.$apply() , cela signifie votre $scope.$apply() n'est pas assez haut dans la pile d'appel.
638
répondu Lee 2016-09-01 16:49:17

D'une discussion récente avec les gars angulaires sur ce même sujet: pour des raisons d'épreuve de l'avenir, vous ne devriez pas utiliser $$phase

lorsqu'on insiste sur la" bonne "façon 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.
})

j'ai récemment rencontré cela en écrivant des services angulaires pour envelopper les API facebook, google, et twitter qui, à des degrés divers, ont des callbacks remis.

voici un exemple à partir de l'intérieur d'un service. (Par souci de concision, le reste du service -- définir des variables, injecté $timeout etc. -- a été laissé.)

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 delay pour $timeout est optionnel et qu'il sera par défaut à 0 s'il n'est pas activé ( $timeout calls $browser.reporter qui par défaut est 0 si le retard n'est pas défini )

un peu non intuitif, mais c'est la réponse des gars qui écrivent Angular, donc ça me suffit!

636
répondu betaorbust 2013-09-25 04:06:19

le cycle de digestion est un appel synchrone. Il ne cédera pas le contrôle à la boucle d'événements du navigateur jusqu'à ce que ce soit fait. Il existe quelques moyens pour y remédier. La façon la plus facile de gérer cela est d'utiliser le $timeout intégré, et une seconde façon est si vous utilisez un underscore ou un lodash (et vous devriez l'être), appelez le suivant:

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

ou si vous avez underscore:

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

nous avons essayé plusieurs solutions de rechange, et nous avons détesté injecter $ rootScope dans tous nos contrôleurs, directives, et même dans certaines usines. Ainsi, l' $timeout "et"_".reporter ont été notre préféré à ce jour. Ces méthodes indiquent avec succès à angular d'attendre jusqu'à la prochaine boucle d'animation, ce qui garantira que la portée actuelle.$s'appliquent.

314
répondu frosty 2015-01-09 22:29:02

beaucoup de réponses ici contiennent de bons conseils mais peuvent aussi conduire à la confusion. Utiliser simplement $timeout est et non la meilleure solution ni la bonne. Aussi, assurez-vous de lire que si vous êtes préoccupé par les performances ou l'évolutivité.

choses que vous devriez savoir

  • $$phase est privé du cadre et il y a de bonnes raisons pour cela.

  • $timeout(callback) attendra que le cycle de digestion actuel (s'il y en a) soit terminé, puis exécutera le callback, puis lancera à la fin un $apply complet .

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

  • $scope.$apply(callback) invoque, entre autres choses, $rootScope.$digest , ce qui signifie qu'il va refaire 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 son champ parent, qui peut enregistrer beaucoup de performances en travaillant sur une partie isolée de votre HTML avec un champ isolé (à 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 va probablement résoudre la plupart de vos problèmes. Veuillez vous reporter au dernier paragraphe pour en savoir plus.

  • si vous obtenez le $digest already in progress error , alors votre architecture est erronée: soit vous n'avez pas besoin de réviser votre portée, ou vous ne devriez pas être en charge de ce (voir ci-dessous).

comment structurer votre code

quand 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 point, vous n'êtes pas en charge de traiter 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 plus de $sur appliquer pour enregistrer les spectacles.

mise à jour depuis Angularjs 1.2

une nouvelle méthode puissante a été ajoutée à tout $scope: $evalAsync . Fondamentalement, il exécutera son callback dans le cycle de digest courant si l'un d'eux se produit, sinon un nouveau cycle de digest commencera à exécuter le callback.

Qui n'est toujours pas aussi bon qu'un $scope.$digest si vous savez vraiment que vous avez seulement besoin de synchroniser un partie isolée de votre HTML (car un nouveau $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 savoir si sera exécuté de manière synchrone ou pas , par exemple après avoir récupéré une ressource potentiellement mise en cache: parfois, cela nécessitera un appel async à 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 )

258
répondu floribon 2015-07-21 19:43:53

peu Maniable méthode d'aide à garder le processus SEC:

function safeApply(scope, fn) {
    (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}
86
répondu lambinator 2013-06-14 18:14:15

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

le problème se pose lorsque vous avez un appel à $apply qui est parfois exécuté asynchrone en dehors du code angulaire (quand $apply devrait être utilisé) et parfois synchrone à l'intérieur du code angulaire (ce qui provoque l'erreur $digest already in progress ).

cela peut arriver, par exemple, lorsque vous avez une bibliothèque qui récupère de manière asynchrone des éléments d'un serveur et les caches. 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 de sorte qu'il peut être récupéré de façon synchrone.

la façon de prévenir cette erreur est de s'assurer que le code qui appelle $apply est exécuté de façon asynchrone. Cela peut être fait en lançant votre code à l'intérieur d'un appel à $timeout avec le retard défini à 0 (qui est la valeur par défaut). Toutefois, l'appel de votre code à l'intérieur de $timeout supprime la nécessité d'appeler $apply , parce que $timeout déclenchera un autre cycle $digest seul, qui fera, à son tour, toutes les mises à jour nécessaires, 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...

faites ceci:

... your controller code...

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

... more of your controller code...

n'appelez $apply que si vous connaissez le code qui l'exécute. être exécuté en dehors de l'Angulaire de code (par exemple, votre appel à $s'appliquent va se passer à l'intérieur d'un callback qui est appelée par le code de l'extérieur de votre Angulaire de code).

à moins que quelqu'un ne soit conscient d'un inconvénient majeur à utiliser $timeout au-dessus de $apply , Je ne vois pas pourquoi vous ne pouviez pas toujours utiliser $timeout (avec un délai zéro) au lieu de $apply , car il fera à peu près la même chose.

32
répondu Trevor 2015-11-17 23:37:16

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 de safeApply mentionnées ici n'ont pas résolu l'erreur pour moi.

mais ce qui l'a résolu, c'est d'utiliser le 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 qu'il est à l'intérieur d'un contrôleur de directive d'usine ou juste besoin d'une sorte de reliure, 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
répondu Ciul 2016-09-13 07:52:34

lorsque vous obtenez cette erreur, cela signifie essentiellement qu'elle est déjà en cours de mise à jour de votre vue. Vous ne devriez vraiment pas avoir besoin d'appeler $apply() dans votre contrôleur. Si votre vue n'est pas mise à jour comme vous vous y attendiez, et que vous obtenez cette erreur après avoir appelé $apply() , cela signifie très probablement que vous ne mettez pas le modèle à jour correctement. Si vous postez quelques détails, nous pourrions résoudre le problème principal.

28
répondu dnc253 2012-10-04 14:41:41

la forme la plus courte de sécurité $apply est:

$timeout(angular.noop)
14
répondu Warlock 2015-11-12 05:04:44

vous pouvez également utiliser evalAsync. Il sera lancé une fois digest terminé!

scope.evalAsync(function(scope){
    //use the scope...
});
11
répondu CMCDragonkai 2013-09-19 01:48:42

parfois vous obtiendrez encore des erreurs si vous utilisez cette façon ( https://stackoverflow.com/a/12859093/801426 ).

essayez ceci:

if(! $rootScope.$root.$$phase) {
...
9
répondu bullgare 2017-05-23 10:31:39

tout d'abord, ne le fixez pas de cette façon

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

ça n'a pas de sens parce que $phase est juste un drapeau booléen pour le cycle $digest, donc votre $apply() ne fonctionnera pas toujours. Et rappelez-vous que c'est un mauvais entraînement.

à la place, 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(); 
});
7
répondu M Sagar 2018-03-05 09:37:27

vous devez utiliser $evalAsync ou $timeout selon le contexte.

C'est un lien avec une bonne explication:

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

5
répondu Luc 2015-11-12 05:04:58

je vous conseille d'utiliser un événement personnalisé plutôt que de déclencher un recueil de cycle.

j'en suis venu à la conclusion que la diffusion d'événements personnalisés et l'enregistrement des auditeurs pour ces événements est une bonne solution pour déclencher une action que vous souhaitez se produire, que vous soyez ou non dans un cycle de digest.

en créant un événement personnalisé, vous êtes aussi plus efficace avec votre code parce que vous ne déclenchez que les auditeurs abonnés à cet événement et non déclenchez toutes les montres reliées à la portée comme vous le feriez si vous invoquiez la portée.$appliquer.

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


$scope.$broadcast('customEventName', optionalCustomEventArguments);
4
répondu nelsonomuto 2014-01-02 15:26:46

yearofmoo a fait un excellent travail à la création d'une fonction réutilisable $safeApply 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);
3
répondu RNobel 2015-11-12 05:05:35

j'ai été en mesure de résoudre ce problème en appelant $eval au lieu de $apply dans les endroits où je sais que la fonction $digest sera en cours d'exécution.

selon les docs , $apply fait essentiellement ceci:

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

dans mon cas, un ng-click change une variable dans un champ d'application, et une montre $sur cette variable change d'autres variables qui doivent être $applied . Ce dernier étape provoque l'erreur "digérer déjà en cours".

en remplaçant $apply par $eval dans l'expression watch, les variables scope sont mises à jour comme prévu.

par conséquent, il apparaît que si digest va être en cours d'exécution de toute façon en raison d'un autre changement dans L'angle, $eval 'ing est tout ce que vous devez faire.

2
répondu teleclimber 2014-03-26 19:08:00

utiliser $scope.$$phase || $scope.$apply(); au lieu de

2
répondu Visakh B Sujathan 2015-11-12 05:05:14

essayer d'utiliser

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

au lieu de

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

$applyAsync Calendrier de l'invocation de $s'appliquent à se produire à une date ultérieure. Cela peut être utilisé pour mettre en file d'attente des expressions multiples qui doivent être évaluées dans le même condensé.

NOTE: dans le $digest, $applyAsync() ne sortira que si la portée actuelle est $rootScope. Cela signifie que si vous appelez $ digest sur une portée enfant, il ne sera pas $applyAsync() de la file d'attente.

Exemple:

  $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. champ d'application.$ applyAsync () vs. Scope.$ evalAsync() in AngularJS 1.3

  1. AngularJs Docs
2
répondu Eduardo Eljaiek 2018-03-29 14:34:17

comprenant que les documents angulaires appellent la vérification du $$phase an anti-pattern , j'ai essayé de faire fonctionner $timeout et _.defer .

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

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 elle est décrite comme un piratage par les gens dans les commentaires et l'équipe angulaire. La commande semble précise et facile à lire:

"les digérer, sauf si l'on est déjà en cours"

en Coffee-script 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 une goutte? $safeApply semble bien, mais utilise le $$phase méthode d'inspection, trop.

1
répondu SimplGy 2014-01-03 22:43:27

C'est mon service 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 usage:

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
répondu ranbuch 2015-08-24 05:59:15

j'ai utilisé cette méthode et elle semble fonctionner parfaitement. Cela attend juste la fin du cycle et déclenche apply() . Il suffit d'appeler 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);
  }
}
1
répondu Ashu 2016-08-14 08:10:46

semblable 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();
        }
    };
0
répondu Shawn Dotey 2016-01-12 20:39:08

vous pouvez utiliser

$timeout

pour prévenir l'erreur.

 $timeout(function () {
                        var scope = angular.element($("#myController")).scope();
                        scope.myMethod();
                        scope.$scope();
                    },1);
0
répondu Satish Singh 2017-09-15 10:54:26

a trouvé ceci: https://coderwall.com/p/ngisma où Nathan Walker (près du bas de la page) suggère un décorateur en $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;
      }
    ]);
  }
]);
-3
répondu Warren Davis 2014-04-11 20:00:09

ce sera résoudre votre problème:

if(!$scope.$$phase) {
  //TODO
}
-7
répondu eebbesen 2015-02-12 03:41:45