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?
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
- ne faites pas
if (!$scope.$$phase) $scope.$apply()
, cela signifie votre$scope.$apply()
n'est pas assez haut dans la pile d'appel.
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!
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.
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 )
peu Maniable méthode d'aide à garder le processus SEC:
function safeApply(scope, fn) {
(scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}
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.
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();
}]
)
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.
la forme la plus courte de sécurité $apply
est:
$timeout(angular.noop)
vous pouvez également utiliser evalAsync. Il sera lancé une fois digest terminé!
scope.evalAsync(function(scope){
//use the scope...
});
parfois vous obtiendrez encore des erreurs si vous utilisez cette façon ( https://stackoverflow.com/a/12859093/801426 ).
essayez ceci:
if(! $rootScope.$root.$$phase) {
...
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();
});
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
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);
yearofmoo a fait un excellent travail à la création d'une fonction réutilisable $safeApply pour nous:
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);
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.
utiliser $scope.$$phase || $scope.$apply();
au lieu de
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
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.
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']);
};
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);
}
}
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();
}
};
vous pouvez utiliser
$timeout
pour prévenir l'erreur.
$timeout(function () {
var scope = angular.element($("#myController")).scope();
scope.myMethod();
scope.$scope();
},1);
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;
}
]);
}
]);
ce sera résoudre votre problème:
if(!$scope.$$phase) {
//TODO
}