web-dev-qa-db-fra.com

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

Je constate que je dois de plus en plus mettre à jour manuellement ma page vers mon scope depuis la construction d'une application en angulaire.

Le seul moyen que je connaisse pour ce faire est d'appeler $apply() à partir de la portée de mes contrôleurs et directives. Le problème avec ceci est que cela continue à envoyer 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?

797
Lightbulb1

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

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

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

$scope.$$phase renverra "$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 ses enfants, et $apply traitera les observateurs de toutes les portées.

Au point @ dnc253, si vous appelez fréquemment $digest ou $apply, vous risquez de ne pas le faire correctement. Je constate généralement que j'ai besoin de digérer lorsque j'ai besoin de mettre à jour l'état de l'oscilloscope à la suite d'un événement DOM se produisant hors de la portée d'Angular. Par exemple, lorsqu'un modal d'amorçage Twitter devient masqué. Parfois, l'événement DOM se déclenche lorsqu'un $digest est en cours, parfois non. C'est pourquoi j'utilise ce chèque. 

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


D'après les commentaires: De @anddoutoi

angular.js Anti Patterns

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

D'après une discussion récente avec les gars angulaires sur ce même sujet: Pour des raisons de sécurité future, vous ne devriez pas utiliser $$phase

Lorsque vous appuyez 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.
})

Je me suis récemment heurté à cela lors de la rédaction de services angulaires pour envelopper les API de Facebook, Google et Twitter, qui reçoivent des rappels à des degrés divers.

Voici un exemple tiré d'un service. (Par souci de brièveté, le reste du service - qui configure les variables, injecte $ 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 de délai pour $ timeout est facultatif et par défaut à 0 s'il n'est pas défini ( $ timeout appelle $ browser.defer dont 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, alors c'est suffisant pour moi!

645
betaorbust

Le cycle de digestion est un appel synchrone. Il ne cédera pas le contrôle de la boucle d'événements du navigateur jusqu'à ce que cela soit terminé. Il y a plusieurs façons de gérer cela. Le moyen le plus simple de résoudre ce problème consiste à utiliser le délai d'expiration $ intégré. Si vous utilisez un trait de soulignement ou un lodash (vous devriez le faire), appelez le numéro suivant:

$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 avons détesté l’injection de $ rootScope dans tous nos contrôleurs, directives et même dans certaines usines. Donc, les $ timeout et _.defer ont été nos préférés jusqu'à présent. Ces méthodes permettent à Angular d'attendre la prochaine boucle d'animation, ce qui garantira que l'étendue actuelle. $ Apply est terminée. 

318
frosty

Beaucoup de réponses ici contiennent de bons conseils, mais peuvent également semer la confusion. Utiliser simplement $timeout est pas le meilleur ni la bonne solution . Assurez-vous également de le lire si vous êtes préoccupé par les performances ou l'évolutivité.

Ce qu'il faut savoir

  • $$phase est une propriété privée du framework et cela pour de bonnes raisons.

  • $timeout(callback) attendra que le cycle de résumé 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 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 redigester la portée racine de l'application et de tous ses enfants, même si vous vous trouvez 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 économiser beaucoup de performances lorsque vous travaillez sur une partie isolée de votre code HTML avec une portée isolée (principalement une directive). $ 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. Veuillez vous reporter au dernier paragraphe pour en savoir plus à ce sujet.

  • si vous obtenez le $digest already in progress error, alors votre architecture est fausse: vous n'avez pas besoin de redéfinir votre portée, ou 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 étendue alors qu'elle est déjà en cours: puisque vous ne connaissez pas l'état de votre étendue à ce stade, vous n'êtes pas responsable du traitement 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 travaillez sur une petite directive isolée faisant partie d'une grande application Angular, vous préférerez peut-être $ digest à la place de $ apply pour enregistrer des performances.

Mise à jour depuis Angularjs 1.2

Une nouvelle méthode puissante a été ajoutée à toute variable $ scope: $evalAsync. Fondamentalement, il exécutera son rappel dans le cycle de synthèse en cours, le cas échéant, sinon un nouveau cycle de synthèse commencera à exécuter le rappel.

Ce n'est toujours pas aussi bon qu'un $scope.$digest si vous savez vraiment qu'il vous suffit de synchroniser une partie isolée de votre code HTML (puisqu'un nouveau $apply sera déclenché si aucun n'est en cours), mais c'est la meilleure solution lorsque vous exécutez une fonction que vous ne pouvez pas savoir si 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 extraite localement de manière synchrone.

Dans ces cas et dans tous les autres où vous aviez un !$scope.$$phase, veillez à utiliser $scope.$evalAsync( callback )

262
floribon

Méthode peu pratique d'assistance pour garder ce processus au sec: 

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

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

Le problème survient lorsque vous appelez $apply qui est parfois exécuté de manière asynchrone en dehors du code angulaire (lorsque $ apply doit être utilisé) et parfois de manière synchrone dans le code angulaire (ce qui provoque l'erreur $digest already in progress).

Cela peut arriver, par exemple, lorsque vous avez une bibliothèque qui extrait de manière 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 de 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.

Pour éviter cette erreur, vous devez vous 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 défini à 0 (qui est la valeur par défaut). Cependant, appeler votre code à l'intérieur de $timeout supprime la nécessité d'appeler $apply, car $ timeout déclenchera un autre cycle $digest, qui effectuera à 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...

faire ceci:

... your controller code...

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

... more of your controller code...

Appelez uniquement $apply lorsque vous connaissez le code qui l’exécute s’exécutera toujours en dehors du code angulaire (par exemple, votre appel à $ apply se fera dans un rappel appelé par code en dehors de votre code angulaire).

À moins que quelqu'un ne soit conscient de certains inconvénients importants liés à l'utilisation de $timeout par rapport à $apply, je ne vois pas pourquoi vous ne pourriez pas toujours utiliser $timeout (sans délai) au lieu de $apply, car cela ferait à peu près la même chose.

32
Trevor

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

Mais ce qui a résolu le problème est d'utiliser le service $ timeout (n'oubliez pas de l'injecter d'abord).

Ainsi, quelque chose comme:

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

et si à l'intérieur de votre code que vous utilisez 

ce

peut-être parce que cela se trouve dans le contrôleur d'une directive d'usine ou si vous avez simplement besoin d'une sorte de liaison, vous feriez alors 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
Ciul

Lorsque vous obtenez cette erreur, cela signifie qu’elle est déjà en train de mettre à jour votre vue. Vous ne devriez vraiment pas avoir besoin d'appeler $apply() depuis votre contrôleur. Si votre vue ne se met pas à jour comme prévu et si vous obtenez cette erreur après avoir appelé $apply(), cela signifie probablement que vous ne mettez pas à jour le modèle correctement. Si vous publiez des détails, nous pourrions trouver le problème principal.

28
dnc253

La forme la plus courte de $apply sûr est:

$timeout(angular.noop)
14
Warlock

Vous pouvez également utiliser evalAsync. Il fonctionnera quelque temps après la fin de la digestion!

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

Parfois, vous aurez toujours des erreurs si vous utilisez cette méthode ( https://stackoverflow.com/a/12859093/801426 ).

Essaye ça:

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

Tout d’abord, ne le corrige pas de cette façon

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

Cela n’a aucun sens car $ phase n’est qu’un indicateur booléen pour le cycle $ digest, de sorte que votre $ apply () ne fonctionne parfois pas. Et rappelez-vous que c’est une mauvaise pratique.

Utilisez plutôt $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 un trait de soulignement ou lodash, vous pouvez utiliser defer ():

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

Vous devez utiliser $ evalAsync ou $ timeout en fonction du contexte.

Ceci est un lien avec une bonne explication: 

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

5
Luc

Je vous conseillerais d'utiliser un événement personnalisé plutôt que de déclencher un cycle de résumé.

Je me suis rendu compte que la diffusion d'événements personnalisés et l'enregistrement d'auditeurs pour ces événements sont une bonne solution pour déclencher une action que vous souhaitez réaliser, que vous soyez ou non dans un cycle de résumé. 

En créant un événement personnalisé, votre code est également plus efficace, car vous ne faites que déclencher des écouteurs abonnés à cet événement et PAS tous les contrôles liés à l'étendue comme vous le feriez si vous appeliez l'étendue.

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


$scope.$broadcast('customEventName', optionalCustomEventArguments);
4
nelsonomuto

yearofmoo a fait un excellent travail en créant 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
RNobel

J'ai pu résoudre ce problème en appelant $eval au lieu de $apply à des endroits où je sais que la fonction $digest sera en cours d'exécution.

Selon le 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 modifie une variable dans une étendue et un $ watch sur cette variable modifie d'autres variables qui doivent être $applied. Cette dernière étape provoque l'erreur "digérer déjà en cours".

En remplaçant $apply par $eval dans l'expression de contrôle, les variables d'étendue sont mises à jour comme prévu.

Par conséquent, apparaît que si digest doit être exécuté de toute façon à cause d'un autre changement dans Angular, $eval 'ing est tout ce que vous devez faire.

2
teleclimber

utilisez $scope.$$phase || $scope.$apply(); à la place

2

essayez d'utiliser 

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

au lieu de

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

$ applyAsync Planifiez l'invocation de $ apply pour qu'elle se produise ultérieurement. Cela peut être utilisé pour mettre en file d'attente plusieurs expressions devant être évaluées dans le même condensé.

REMARQUE: Dans le $ digest, $ applyAsync () se videra uniquement si l'étendue actuelle est $ rootScope. Cela signifie que si vous appelez $ digest sur une étendue enfant, cela ne videra pas implicitement la file d'attente $ applyAsync ().

Exemple:

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

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

Références: 

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

  1. Documents AngularJs
2
Eduardo Eljaiek

J'utilise cette méthode et elle semble fonctionner parfaitement. Cela attend simplement le temps que le cycle se termine et déclenche ensuite apply(). Appelez simplement la fonction apply(<your scope>) de n’importe où.

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
Ashu

Comprenant que l'appel de documents Angular vérifiant le $$phase un anti-motif , j'ai essayé de faire fonctionner $timeout et _.defer.

Les méthodes de délai d'expiration et différé créent un flash de contenu {{myVar}} non analysé dans le dom, comme un FOUT . Pour moi, ce n'était pas acceptable. Cela me laisse peu de choses à dire dogmatiquement que quelque chose est un bidouillage 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, ni pourquoi elle est décrite comme un piratage par des personnes dans les commentaires et l'équipe angular. La commande semble précise et facile à lire:

"Faites le résumé sauf si cela se produit déjà"

Dans CoffeeScript, c'est encore plus joli:

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

Quel est le problème avec cela? Existe-t-il une alternative qui ne crée pas de FOUT? $ safeApply semble correct mais utilise également la méthode d'inspection $$phase.

1
SimplGy

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

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
Shawn Dotey

Vous pouvez utiliser 

$timeout

pour éviter l'erreur. 

 $timeout(function () {
                        var scope = angular.element($("#myController")).scope();
                        scope.myMethod();
                        scope.$scope();
                    },1);
0
Satish Singh