guide - templateurl angularjs




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

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

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'attivazione 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 attiva 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.

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

Prova questo:

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

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 $browser.defer 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!



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(<your scope>) 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);
  }
}

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 angolare 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à attivata 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);
}

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

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

Puoi usare

$timeout

per prevenire l'errore.

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

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

Questo risolverà il tuo problema:

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

Ti consiglierei di utilizzare un evento personalizzato anziché attivare 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'attivazione 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 attivando solo gli ascoltatori iscritti a detto evento e NON attivando 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);

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

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

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

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




angular-digest