858 votes

AngularJS : Prévenir l'erreur $digest already in progress lors de l'appel de $scope.$apply()

Je constate que j'ai de plus en plus besoin de mettre à jour manuellement ma page vers ma portée depuis que je construis une application en angulaire.

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 est qu'il continue à envoyer une erreur dans la console qui se lit comme suit :

Erreur : $digest déjà en cours

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

34 votes

C'est vraiment frustrant de devoir utiliser $apply de plus en plus.

0 votes

J'obtiens également cette erreur, même si j'appelle $apply dans un callback. J'utilise une bibliothèque tierce pour accéder à des données sur leurs serveurs, je ne peux donc pas tirer parti de $http, et je ne veux pas le faire puisque je devrais réécrire leur bibliothèque pour utiliser $http.

45 votes

Utiliser $timeout()

675voto

betaorbust Points 2428

D'une discussion récente avec les gars d'Angular sur ce même sujet : Pour des raisons de pérennité, vous ne devriez pas utiliser $$phase

Lorsqu'on leur demande quelle est la "bonne" façon de procéder, la réponse est actuellement la suivante

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

J'ai récemment rencontré ce problème en écrivant des services angulaires pour envelopper les API facebook, google et twitter qui, à des degrés divers, ont des callbacks.

Voici un exemple à l'intérieur d'un service. (Pour des raisons de brièveté, le reste du service -- qui configure les variables, injecte $timeout etc. -- a été laissé de cô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 délai pour $timeout est facultatif et prendra la valeur 0 par défaut s'il n'est pas défini ( $timeout appelle $browser.defer dont La valeur par défaut est 0 si le délai n'est pas défini. )

Ce n'est pas très intuitif, mais c'est la réponse des auteurs d'Angular, donc ça me suffit !

5 votes

J'ai rencontré ce problème à plusieurs reprises dans mes directives. J'en écrivais une pour redactor et cela s'est avéré fonctionner parfaitement. J'étais à un meetup avec Brad Green et il a dit qu'Angular 2.0 sera énorme, sans cycle de digestion, en utilisant la capacité d'observation native de JS et en utilisant un polyfill pour les navigateurs qui n'en ont pas. À ce moment-là, nous n'aurons plus besoin de faire ça :)

0 votes

Hier, j'ai constaté un problème où l'appel à selectize.refreshItems() à l'intérieur de $timeout a causé la redoutable erreur de récurrence des données. Une idée de comment cela pourrait être ?

0 votes

L'utilisation de $timeout fonctionne, mais les performances sont très mauvaises. Si vous avez beaucoup de directives, il rafraîchira tout.

671voto

Lee Points 3536

N'utilisez pas ce modèle - Cela finira par causer plus d'erreurs que cela n'en résout. Même si vous pensez avoir résolu 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 retournera "$digest" o "$apply" si un $digest o $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 scopes.

Pour répondre à la remarque de @dnc253, si vous vous retrouvez à appeler $digest o $apply fréquemment, vous le faites peut-être mal. Je trouve généralement que j'ai besoin de digérer lorsque je dois mettre à jour l'état de la portée à la suite d'un événement DOM déclenché hors de la portée d'Angular. Par exemple, lorsqu'une modale twitter bootstrap devient cachée. Parfois, l'événement DOM se déclenche lorsqu'une $digest est en cours, parfois non. C'est pourquoi j'utilise cette vérification.

J'aimerais connaître une meilleure méthode si quelqu'un en connaît une.


Des commentaires : par @anddoutoi

angular.js Anti Patterns

  1. Ne le faites pas. if (!$scope.$$phase) $scope.$apply() cela signifie que votre $scope.$apply() n'est pas assez haut dans la pile d'appels.

1 votes

Merci ! Cela m'a sauvé la mise. Je gérais des événements de jQuery qui étaient déclenchés par des éléments d'une liste sur laquelle se trouvait un filtre. J'ai eu cette erreur lorsque j'ai modifié le contenu d'un des éléments de cette liste. Je me suis beaucoup grattée sur ce problème et j'ai cherché spécifiquement un moyen de détecter si un $digest était en cours. La documentation est inutile pour ce genre de choses.

231 votes

Il me semble que $digest / $apply devrait le faire par défaut.

21 votes

Notez que dans certains cas, je dois vérifier la portée actuelle ET la portée racine. J'ai obtenu une valeur pour $$phase sur le Root mais pas sur mon scope. Je pense que cela a quelque chose à voir avec la portée isolée d'une directive, mais

332voto

aaronfrost Points 4700

Le cycle de digestion est un appel synchrone. Il ne cède pas le contrôle à la boucle d'événements du navigateur tant qu'il n'est pas terminé. Il existe plusieurs façons de gérer ce problème. La façon la plus simple est d'utiliser le $timeout intégré, et une deuxième façon est si vous utilisez underscore ou lodash (et vous devriez le faire), appelez ce qui suit :

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

ou si vous avez lodash :

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

Nous avons essayé plusieurs solutions de contournement, et nous avons détesté injecter $rootScope dans tous nos contrôleurs, directives, et même dans certaines fabriques. Ainsi, les méthodes $timeout et _.defer ont été nos préférées jusqu'à présent. Ces méthodes indiquent avec succès à angular d'attendre la prochaine boucle d'animation, ce qui garantira que le scope.$apply actuel est terminé.

1 votes

_.defer attend le prochain cycle d'événement. Le digest actuel sera terminé d'ici là, donc le $scope.$apply sera libre de s'exécuter, puisqu'il n'y aura pas de digest en cours.

1 votes

@aaronfrost : Super ! Pour le débogage : console.log('digest? : ', !!$scope.$$phase || !$scope.$Root.$$phase) ; _.defer(function() { console.log('digest? : ', !!$scope.$$phase || !$scope.$Root.$$phase) ; }) ; La première instruction renvoie true si le $digest est en cours de traitement ; la seconde renvoie false, car elle est exécutée lorsque la boucle d'événement a déclenché toutes les tâches en attente.

2 votes

Est-ce comparable à l'utilisation de $timeout(...) ? J'ai utilisé $timeout dans plusieurs cas pour reporter au prochain cycle d'événement et cela semble bien fonctionner - quelqu'un sait-il s'il y a une raison de ne pas utiliser $timeout ?

270voto

floribon Points 1149

Bon nombre des réponses données ici contiennent de bons conseils mais peuvent également prêter à confusion. Il suffit d'utiliser $timeout es no la meilleure ni la bonne solution. N'oubliez pas non plus de lire cela si vous êtes préoccupé par les performances ou l'évolutivité.

Ce que vous devez savoir

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

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

  • $timeout(callback, delay, false) fera la même chose (avec un délai optionnel avant l'exécution de la fonction de rappel), mais ne déclenchera pas d'action de rappel. $apply (troisième argument) qui sauve 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 redigestera l'étendue de la racine de l'application et tous ses enfants, même si vous vous trouvez dans une étendue 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 économiser 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 callback : vous exécutez le code, puis vous digérez.

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

  • si vous obtenez le $digest already in progress error alors votre architecture est erronée : soit vous n'avez pas besoin de redigester votre champ d'application, soit tu ne devrais pas être en charge de ça (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 en charge de vous occuper de 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 que vous travaillez sur une petite directive isolée dans le cadre d'une grande application Angular, vous pouvez préférer $digest à $apply pour économiser des performances.

Mise à jour depuis Angularjs 1.2

Une nouvelle méthode puissante a été ajoutée à tout $scope : $evalAsync . Fondamentalement, il exécutera son rappel dans le cycle de digestion actuel s'il y en a un, sinon un nouveau cycle de digestion commencera à exécuter le rappel.

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

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

4 votes

$timeout est critiquée en passant. Pouvez-vous donner d'autres raisons d'éviter $timeout ?

89voto

lambinator Points 2876

Une petite méthode pratique pour garder ce processus SEC :

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

6 votes

Votre SafeApply m'a aidé à comprendre ce qui se passait, plus que toute autre chose. Merci de l'avoir posté.

4 votes

J'étais sur le point de faire la même chose, mais cela ne signifie-t-il pas qu'il y a une chance que les changements que nous faisons dans fn() ne soient pas vus par $digest ? Ne serait-il pas préférable de retarder la fonction, en supposant que scope.$$phase === '$digest' ?

0 votes

Je suis d'accord, parfois $apply() est utilisé pour déclencher le digest, en appelant simplement la fn par elle-même... cela ne va-t-il pas entraîner un problème ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X