AngularJS: impedisce l’errore $ digest già in corso quando si chiama $ scope. $ Apply ()

Sto scoprendo che ho bisogno di aggiornare la mia pagina per il mio ambito manualmente sempre di più poiché si costruisce un’applicazione angular.

L’unico modo che conosco per farlo è chiamare $apply() dall’ambito dei miei controller e direttive. Il problema con questo è che continua a generare un errore nella console che legge:

Errore: $ digest già in corso

Qualcuno sa come evitare questo errore o ottenere la stessa cosa ma in un modo diverso?

    Non usare questo modello – Questo finirà per causare più errori di quanti ne risolva. Anche se pensi che sia stato corretto qualcosa, non è così.

    Puoi verificare se un $digest è già in corso controllando $scope.$$phase .

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

    $scope.$$phase restituirà "$digest" o "$apply" se sono in corso $digest o $apply . Credo che la differenza tra questi stati sia che $digest elaborerà gli orologi dello scope corrente e dei suoi figli e $apply processerà gli osservatori di tutti gli ambiti.

    Per il punto @ dnc253, se ti trovi a chiamare $digest o $apply frequentemente, potresti sbagliarti. Generalmente ho bisogno di digerire quando ho bisogno di aggiornare lo stato dell’oscilloscopio a seguito dell’triggerszione di un evento DOM fuori dalla portata di Angular. Ad esempio, quando un modal bootstrap di Twitter diventa nascosto. A volte l’evento DOM si triggers quando è in corso un $digest , a volte no. Ecco perché uso questo assegno.

    Mi piacerebbe conoscere un modo migliore se qualcuno ne conosce uno.


    Dai commenti: per @anddoutoi

    angular.js Anti Patterns

    1. Non fare if (!$scope.$$phase) $scope.$apply() , significa $scope.$apply() non è abbastanza alto nello stack di chiamate.

    Da una discussione recente con i ragazzi di Angular su questo argomento: per motivi di prova del futuro, non dovresti usare la $$phase

    Se premuto per il “giusto” modo di farlo, la risposta è al momento

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

    Di recente mi sono imbattuto in questo quando scrivevo servizi angolari per avvolgere le API di facebook, google e twitter che, a vari livelli, hanno restituito i callback.

    Ecco un esempio all’interno di un servizio. (Per ragioni di brevità, il resto del servizio – che imposta variabili, timeout $ iniettato ecc. – è stato interrotto.)

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

    Si noti che l’argomento del ritardo per $ timeout è facoltativo e verrà impostato su 0 se lasciato non impostato ( $ timeout chiama $ browser.defer che assume come valore predefinito 0 se il ritardo non è impostato )

    Un po ‘non intuitivo, ma questa è la risposta dei ragazzi che scrivono Angular, quindi è abbastanza buono per me!

    Il ciclo di digest è una chiamata sincrona. Non arriverà al controllo del ciclo degli eventi del browser fino a quando non sarà completato. Ci sono alcuni modi per affrontare questo. Il modo più semplice per risolvere questo problema è utilizzare il timeout incorporato in $, e un secondo modo è se si utilizza underscore o lodash (e si dovrebbe essere), chiamare il seguente:

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

    o se hai underscore:

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

    Abbiamo provato diversi metodi e abbiamo odiato l’iniezione di $ rootScope in tutti i nostri controller, direttive e persino in alcune fabbriche. Quindi, il timeout $ e _.defer sono stati i nostri preferiti finora. Questi metodi dicono con precisione angular di attendere il prossimo ciclo di animazione, che garantirà che l’ambito attuale. $ Apply sia finito.

    Molte delle risposte qui contengono buoni consigli ma possono anche portare a confusione. Semplicemente usando $timeout non è la soluzione migliore né la soluzione giusta. Inoltre, assicurati di leggere questo se sei preoccupato per le prestazioni o la scalabilità.

    Cose che dovresti sapere

    • $$phase è privata per il quadro e ci sono buone ragioni per questo.

    • $timeout(callback) attenderà fino al termine del ciclo digest (se presente), quindi eseguirà il callback, quindi eseguirà alla fine un $apply intero.

    • $timeout(callback, delay, false) farà lo stesso (con un ritardo opzionale prima di eseguire il callback), ma non sparerà un $apply (terzo argomento) che salva le prestazioni se non hai modificato il tuo modello Angular ($ scope ).

    • $scope.$apply(callback) richiama, tra le altre cose, $rootScope.$digest , il che significa che ridigigherà lo scope di root dell’applicazione e tutti i suoi figli, anche se ci si trova in un ambito isolato.

    • $scope.$digest() semplicemente sincronizzerà il suo modello con la vista, ma non digerirà l’ambito dei suoi genitori, il che può far risparmiare molte prestazioni quando si lavora su una parte isolata del tuo HTML con un ambito isolato (principalmente da una direttiva) . $ digest non accetta una richiamata: si esegue il codice, quindi si digerisce.

    • $scope.$evalAsync(callback) è stato introdotto con angularjs 1.2 e probabilmente risolverà la maggior parte dei tuoi problemi. Si prega di fare riferimento all’ultimo paragrafo per saperne di più.

    • se ottieni l’ $digest already in progress error , allora la tua architettura è sbagliata: o non hai bisogno di ridigestare il tuo scope, o non dovresti esserne responsabile (vedi sotto).

    Come strutturare il tuo codice

    Quando ricevi questo errore, stai provando a digerire il tuo scope mentre è già in corso: dato che non conosci lo stato del tuo scope in quel momento, non sei incaricato di gestire la sua digestione.

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

    E se sai cosa stai facendo e lavori su una piccola direttiva isolata mentre fai parte di una grande applicazione Angolare, potresti preferire $ digest invece di $ apply per salvare le performance.

    Aggiornamento da Angularjs 1.2

    Un nuovo, potente metodo è stato aggiunto a qualsiasi $ scope: $evalAsync . Fondamentalmente, eseguirà il suo callback nel ciclo digest corrente se ne sta avvenendo uno, altrimenti un nuovo ciclo digest inizierà ad eseguire il callback.

    Ciò non è ancora buono come $scope.$digest se sai davvero che devi solo sincronizzare una parte isolata del tuo HTML (dal momento che una nuova $apply verrà triggersta se nessuno è in corso), ma questo è il migliore soluzione quando si esegue una funzione che non si può conoscere se eseguita in modo sincrono o meno , ad esempio dopo aver recuperato una risorsa potenzialmente memorizzata nella cache: a volte ciò richiederà una chiamata asincrona a un server, altrimenti la risorsa verrà prelevata localmente in modo sincrono.

    In questi casi e in tutti gli altri in cui hai avuto una fase !$scope.$$phase , assicurati di utilizzare $scope.$evalAsync( callback )

    Pratico metodo di supporto per mantenere questo processo ASCIUTTO:

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

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

    Il problema sorge quando si ha una chiamata a $apply che a volte viene eseguita in modo asincrono al di fuori del codice angular (quando $ apply deve essere usato) e talvolta in modo sincrono all’interno del codice angular (che causa l’errore $digest already in progress ).

    Ciò può accadere, ad esempio, quando si dispone di una libreria che recupera in modo asincrono gli elementi da un server e li memorizza nella cache. La prima volta che viene richiesto un articolo, questo verrà recuperato in modo asincrono per non bloccare l’esecuzione del codice. La seconda volta, tuttavia, l’elemento è già nella cache, quindi può essere recuperato in modo sincrono.

    Il modo per prevenire questo errore è garantire che il codice che chiama $apply sia eseguito in modo asincrono. Questo può essere fatto eseguendo il codice all’interno di una chiamata a $timeout con il ritardo impostato su 0 (che è l’impostazione predefinita). Tuttavia, la chiamata del codice all’interno di $timeout rimuove la necessità di chiamare $apply , perché $ timeout attiverà un altro ciclo $digest da solo, che a sua volta farà tutto l’aggiornamento necessario, ecc.

    Soluzione

    In breve, invece di fare questo:

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

    Fai questo:

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

    Richiama $apply solo quando sai che il codice in esecuzione verrà sempre eseguito al di fuori del codice Angular (ad esempio, la tua chiamata a $ apply avverrà all’interno di una richiamata che viene chiamata per codice al di fuori del tuo codice angular).

    A meno che qualcuno non sia a conoscenza di uno svantaggio di impatto sull’utilizzo di $timeout su $apply , non vedo perché non si possa sempre usare $timeout (con ritardo zero) anziché $apply , dato che farà approssimativamente la stessa cosa.

    Ho avuto lo stesso problema con script di terze parti come CodeMirror per esempio e Krpano, e anche usando metodi safeApply menzionati qui non ho risolto l’errore per me.

    Ma ciò che ha risolto è l’utilizzo del servizio $ timeout (non dimenticare di iniettarlo prima).

    Quindi, qualcosa come:

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

    e se all’interno del codice che stai utilizzando

    Questo

    forse perché è all’interno del controller di una direttiva di fabbrica o ha solo bisogno di un qualche tipo di binding, quindi dovresti fare qualcosa come:

     .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(); }] ) 

    Quando ricevi questo errore, significa che è già in corso l’aggiornamento della visualizzazione. Davvero non dovresti aver bisogno di chiamare $apply() all’interno del tuo controller. Se la tua vista non si aggiorna come ti aspetteresti, e quindi ricevi questo errore dopo aver chiamato $apply() , molto probabilmente significa che non stai aggiornando il modello correttamente. Se pubblichi alcuni dettagli, potremmo capire il problema principale.

    La forma più corta di $apply sicuri è:

     $timeout(angular.noop) 

    Puoi anche usare evalAsync. Funzionerà qualche volta dopo che il digest è finito!

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

    A volte continuerai a ricevere errori se utilizzi questo modo ( https://stackoverflow.com/a/12859093/801426 ).

    Prova questo:

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

    Prima di tutto, non aggiustarlo in questo modo

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

    Non ha senso perché $ phase è solo un flag booleano per il ciclo $ digest, quindi il tuo $ apply () a volte non verrà eseguito. E ricorda che è una ctriggers pratica.

    Invece, usa $timeout

      $timeout(function(){ // Any code in here will automatically have an $scope.apply() run afterwards $scope.myvar = newValue; // And it just works! }); 

    Se stai usando underscore o lodash, puoi usare defer ():

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

    Dovresti usare $ evalAsync o $ timeout in base al contesto.

    Questo è un link con una buona spiegazione:

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

    Ti consiglierei di utilizzare un evento personalizzato anziché triggersre un ciclo di digestione.

    Sono giunto alla conclusione che la trasmissione di eventi personalizzati e la registrazione di ascoltatori per questi eventi sia una buona soluzione per l’triggerszione di un’azione che desideri venga eseguita o meno in un ciclo di digestione.

    Creando un evento personalizzato, il tuo codice ti sta anche rendendo più efficiente perché stai triggersndo solo gli ascoltatori iscritti a detto evento e NON triggersndo tutti gli orologi associati all’ambito come faresti se avessi invocato scope. $ Apply.

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

    yearofmoo ha fatto un ottimo lavoro nel creare una funzione $ safeApply riutilizzabile per noi:

    https://github.com/yearofmoo/AngularJS-Scope.SafeApply

    Uso:

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

    Sono stato in grado di risolvere questo problema chiamando $eval anziché $apply in posti in cui so che la funzione $digest sarà in esecuzione.

    Secondo i documenti , $apply fondamentalmente questo:

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

    Nel mio caso, un ng-click cambia una variabile all’interno di un ambito e un $ watch su quella variabile modifica altre variabili che devono essere $applied . Quest’ultimo passaggio causa l’errore “digest già in corso”.

    Sostituendo $apply con $eval all’interno dell’espressione watch le variabili scope vengono aggiornate come previsto.

    Pertanto, sembra che se il digest verrà eseguito comunque a causa di qualche altro cambiamento all’interno di Angular, $eval è tutto ciò che devi fare.

    usa $scope.$$phase || $scope.$apply(); $scope.$$phase || $scope.$apply(); anziché

    prova ad usare

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

    invece di

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

    $ applyAsync Pianifica l’invocazione di $ apply per essere eseguita in un secondo momento. Questo può essere usato per mettere in coda più espressioni che devono essere valutate nello stesso digest.

    NOTA: All’interno di $ digest, $ applyAsync () verrà eseguito solo se l’ambito corrente è $ rootScope. Ciò significa che se si chiama $ digest su un ambito figlio, non svuoterà implicitamente la coda $ applyAsync ().

    Exmaple:

      $scope.$applyAsync(function () { if (!authService.authenticated) { return; } if (vm.file !== null) { loadService.setState(SignWizardStates.SIGN); } else { loadService.setState(SignWizardStates.UPLOAD_FILE); } }); 

    Riferimenti:

    1. Ambito. $ ApplyAsync () vs. Scope. $ EvalAsync () in AngularJS 1.3

    1. Documenti AngularJs

    Capendo che i documenti angolari chiamano controllando la $$phase un anti-pattern , ho cercato di ottenere $timeout e _.defer per funzionare.

    I metodi timeout e posticipato creano un lampo di contenuti {{myVar}} nella dom come un FOUT . Per me questo non era accettabile. Mi lascia senza molto da dire dogmaticamente che qualcosa è un hack e non ho un’alternativa adatta.

    L’unica cosa che funziona ogni volta è:

    if(scope.$$phase !== '$digest'){ scope.$digest() } .

    Non capisco il pericolo di questo metodo, o perché è descritto come un trucco da persone nei commenti e dal team angular. Il comando sembra preciso e facile da leggere:

    “Fai il digest a meno che non si stia già succedendo”

    In CoffeeScript è ancora più bello:

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

    Qual è il problema con questo? C’è un’alternativa che non creerà un FOUT? $ safe Apply sembra buono ma utilizza anche il metodo di controllo delle $$phase .

    Questo è il mio servizio di utilità:

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

    e questo è un esempio per il suo utilizzo:

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

    Ho usato questo metodo e sembra funzionare perfettamente bene. Questo attende il tempo in cui il ciclo è terminato e quindi si apply() trigger apply() . Basta chiamare la funzione apply() da qualsiasi luogo tu voglia.

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

    simile alle risposte sopra ma questo ha funzionato fedelmente per me … in un servizio aggiungere:

      //sometimes you need to refresh scope, use this to prevent conflict this.applyAsNeeded = function (scope) { if (!scope.$$phase) { scope.$apply(); } }; 

    Puoi usare

    $timeout

    per prevenire l’errore.

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

    Trovato questo: https://coderwall.com/p/ngisma dove Nathan Walker (vicino alla fine della pagina) suggerisce un decoratore in $ rootScope per creare func ‘safeApply’, codice:

     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; } ]); } ]); 

    Questo risolverà il tuo problema:

     if(!$scope.$$phase) { //TODO }