it-swarm.com.de

AngularJS: Verhindert, dass Fehler $ digest bereits beim Aufruf von $ scope ausgeführt wird. $ Apply ()

Ich stelle fest, dass ich meine Seite immer häufiger manuell aktualisieren muss, seit ich eine Anwendung in eckig erstellt habe.

Die einzige Möglichkeit, dies zu tun, besteht darin, $apply() aus dem Bereich meiner Controller und Direktiven aufzurufen. Das Problem dabei ist, dass immer wieder ein Fehler an die Konsole ausgegeben wird, auf der Folgendes angezeigt wird:

Fehler: $ digest ist bereits in Bearbeitung

Kann jemand diesen Fehler vermeiden oder dasselbe erreichen, aber auf andere Weise?

797
Lightbulb1

Dieses Muster nicht verwenden - Dies führt am Ende zu mehr Fehlern, als es löst. Obwohl Sie denken, dass es etwas repariert hat, ist es nicht der Fall. 

Sie können prüfen, ob bereits ein $digest läuft, indem Sie $scope.$$phase überprüfen. 

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

$scope.$$phase gibt "$digest" oder "$apply" zurück, wenn ein $digest oder $apply ausgeführt wird. Ich glaube, der Unterschied zwischen diesen Zuständen ist, dass $digest die Uhren des aktuellen Geltungsbereichs und seiner untergeordneten Elemente verarbeitet und $apply die Beobachter aller Gültigkeitsbereiche verarbeitet.

Wenn Sie feststellen, dass Sie bei @ dnc253 häufig $digest oder $apply aufrufen, tun Sie dies möglicherweise falsch. Im Allgemeinen finde ich, dass ich verdauen muss, wenn ich den Status des Bereichs aufgrund eines DOM-Ereignisses aktualisieren muss, das außerhalb der Reichweite von Angular ausgelöst wird. Zum Beispiel, wenn ein Twitter-Bootstrap-Modal ausgeblendet wird. Manchmal wird das DOM-Ereignis ausgelöst, wenn ein $digest ausgeführt wird, manchmal nicht. Deshalb verwende ich diesen Check. 

Ich würde gerne einen besseren Weg finden, wenn jemand einen kennt.


Aus den Kommentaren: Von @anddoutoi

angle.js Anti Patterns

  1. Machen Sie nicht if (!$scope.$$phase) $scope.$apply(), es bedeutet, dass Ihre $scope.$apply() nicht hoch genug ist.
645
Lee

Aus einer aktuellen Diskussion mit den Angular-Jungs zu diesem Thema: Aus Gründen der Zukunftssicherung sollten Sie $$phase nicht verwenden.

Wenn Sie für die "richtige" Methode drücken, wird die Antwort aktuell angezeigt

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

Ich habe mich kürzlich mit dem Schreiben von Winkeldiensten beschäftigt, um die APIs von Facebook, Google und Twitter zu verpacken, die in unterschiedlichem Maße Rückrufe eingereicht haben.

Hier ist ein Beispiel aus einem Dienst. (Der Kürze halber wurde der Rest des Dienstes, der Variablen einstellt, $ timeout injiziert usw., weggelassen.)

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

Beachten Sie, dass das Verzögerungsargument für $ timeout optional ist und standardmäßig auf 0 gesetzt wird ( $ timeout ruft $ browser.defer auf, wobei auf 0 gesetzt ist, wenn delay nicht gesetzt ist ).

Ein bisschen nicht intuitiv, aber das ist die Antwort von den Jungs, die Angular schreiben, also ist es gut genug für mich!

645
betaorbust

Der Digest-Zyklus ist ein synchroner Aufruf. Es wird erst dann die Kontrolle über die Ereignisschleife des Browsers übernehmen. Es gibt einige Möglichkeiten, damit umzugehen. Der einfachste Weg, um damit umzugehen, ist das eingebaute $ timeout, und ein zweiter Weg ist, wenn Sie Unterstrich oder lodash verwenden (und Sie sollten es sein), rufen Sie folgendes an:

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

oder wenn Sie einen Unterstrich haben:

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

Wir haben mehrere Problemumgehungen ausprobiert und es haßte es, $ rootScope in all unsere Controller, Direktiven und sogar einige Fabriken einzuführen. Die $ timeout und _.defer waren bisher unser Favorit. Diese Methoden weisen winklig erfolgreich an, bis zur nächsten Animationsschleife zu warten, wodurch sichergestellt wird, dass der aktuelle Gültigkeitsbereich. $ Apply abgeschlossen ist. 

318
frosty

Viele der Antworten enthalten gute Ratschläge, können aber auch zu Verwirrung führen. Die Verwendung von $timeout ist nicht die beste bzw. die richtige Lösung . Lesen Sie auch das unbedingt, wenn Sie von Leistung oder Skalierbarkeit betroffen sind.

Dinge, die du wissen solltest

  • $$phase ist privat in dem Framework und es gibt gute Gründe dafür.

  • $timeout(callback) wartet, bis der aktuelle Digest-Zyklus (falls vorhanden) abgeschlossen ist, führt dann den Rückruf aus und führt am Ende einen vollständigen $apply aus.

  • $timeout(callback, delay, false) wird dasselbe tun (mit einer optionalen Verzögerung, bevor der Callback ausgeführt wird), es wird jedoch kein $apply (drittes Argument) ausgelöst, der die Leistung einspart, wenn Sie Ihr Angular-Modell ($ scope) nicht geändert haben.

  • $scope.$apply(callback) ruft unter anderem $rootScope.$digest auf, was bedeutet, dass der Root-Geltungsbereich der Anwendung und alle zugehörigen untergeordneten Elemente neu definiert werden, selbst wenn Sie sich in einem isolierten Bereich befinden.

  • $scope.$digest() wird das Modell einfach mit der Ansicht synchronisieren, jedoch nicht den Umfang der übergeordneten Elemente verdauen. Dies kann eine Menge Leistung einsparen, wenn an einem isolierten Teil Ihres HTML-Codes mit einem isolierten Bereich gearbeitet wird (meist aus einer Direktive). $ digest nimmt keinen Rückruf an: Sie führen den Code aus und dann den Digest.

  • $scope.$evalAsync(callback) wurde mit anglejs 1.2 eingeführt und wird wahrscheinlich die meisten Probleme lösen. Bitte lesen Sie den letzten Absatz, um mehr darüber zu erfahren.

  • wenn Sie den $digest already in progress error erhalten, ist Ihre Architektur falsch: Entweder müssen Sie Ihren Gültigkeitsbereich nicht erneut prüfen, oder Sie sollten nicht dafür verantwortlich sein (siehe unten).

So strukturieren Sie Ihren Code

Wenn Sie diesen Fehler erhalten, versuchen Sie, Ihr Zielfernrohr zu verdauen, während es bereits in Bearbeitung ist: Da Sie zu diesem Zeitpunkt noch nicht den Status Ihres Bereichs kennen, sind Sie nicht für die Verdauung zuständig.

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

Und wenn Sie wissen, was Sie tun und an einer isolierten kleinen Direktive arbeiten, während Sie Teil einer großen Angular-Anwendung sind, könnten Sie statt der $ -Anweisung $ Digest vorziehen, um Performances zu speichern.

Update seit Angularjs 1.2

Eine neue, leistungsfähige Methode wurde zu jedem Bereich von $ hinzugefügt: $evalAsync. Grundsätzlich führt es seinen Callback innerhalb des aktuellen Digest-Zyklus aus, falls einer auftritt, andernfalls beginnt ein neuer Digest-Zyklus mit der Ausführung des Callbacks.

Das ist immer noch nicht so gut wie ein $scope.$digest, wenn Sie wirklich wissen, dass Sie nur einen isolierten Teil Ihres HTML-Codes synchronisieren müssen (da ein neuer $apply ausgelöst wird, wenn keiner läuft). Dies ist jedoch die beste Lösung, wenn Sie ausführen eine Funktion, die Sie nicht wissen können, ob sie synchron ausgeführt wird oder nicht, zum Beispiel nach dem Abrufen einer potenziell zwischengespeicherten Ressource: Dies erfordert manchmal einen asynchronen Aufruf eines Servers, andernfalls wird die Ressource lokal synchron abgerufen.

Verwenden Sie in diesen und allen anderen Fällen, in denen Sie einen !$scope.$$phase hatten, unbedingt $scope.$evalAsync( callback ).

262
floribon

Praktische kleine Helfer-Methode, um diesen Prozess trocken zu halten: 

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

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

Das Problem tritt auf, wenn Sie einen Aufruf an $apply haben, der manchmal außerhalb von Angular-Code asynchron ausgeführt wird (wenn $ apply verwendet werden sollte) und manchmal synchron innerhalb von Angular-Code (der den $digest already in progress-Fehler verursacht).

Dies kann beispielsweise der Fall sein, wenn Sie über eine Bibliothek verfügen, die Elemente asynchron von einem Server abruft und im Cache speichert. Bei der ersten Anforderung eines Elements wird es asynchron abgerufen, um die Codeausführung nicht zu blockieren. Beim zweiten Mal ist das Element jedoch bereits im Cache, sodass es synchron abgerufen werden kann.

Sie können diesen Fehler verhindern, indem Sie sicherstellen, dass der Code, der $apply aufruft, asynchron ausgeführt wird. Dies kann durch Ausführen des Codes innerhalb eines Aufrufs an $timeout mit einer Verzögerung von 0 (Standardeinstellung) ausgeführt werden. Durch Aufrufen Ihres Codes in $timeout entfällt jedoch die Notwendigkeit, $apply aufzurufen, da $ timeout einen weiteren $digest-Zyklus auslöst, der wiederum alle erforderlichen Aktualisierungen vornimmt usw.

Lösung

Kurz gesagt, anstatt dies zu tun:

... your controller code...

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

... more of your controller code...

mach das:

... your controller code...

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

... more of your controller code...

Rufen Sie nur $apply an, wenn Sie wissen, dass der Code ausgeführt wird. Er wird immer außerhalb des Angular-Codes ausgeführt (z. B. wird Ihr Aufruf an $ apply innerhalb eines Rückrufs ausgeführt, der außerhalb des Angular-Codes durch einen Code aufgerufen wird).

Wenn nicht jemandem ein schwerwiegender Nachteil bei der Verwendung von $timeout über $apply bekannt ist, kann ich nicht verstehen, warum Sie $timeout (ohne Verzögerung) anstelle von $apply nicht immer verwenden können, da dies ungefähr das Gleiche tut.

32
Trevor

Ich hatte das gleiche Problem mit Skripten von Drittanbietern wie CodeMirror zum Beispiel und Krpano, und auch die Verwendung der hier genannten safeApply-Methoden hat den Fehler für mich nicht gelöst.

Was jedoch gelöst wurde, ist die Verwendung des $ timeout-Dienstes (vergessen Sie nicht, ihn zuerst einzuspritzen).

So etwas wie:

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

und wenn in Ihrem Code Sie verwenden 

diese

vielleicht, weil es sich im Controller einer Factory-Direktive befindet oder nur eine Art Bindung benötigt, dann würden Sie Folgendes tun:

.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

Wenn Sie diese Fehlermeldung erhalten, bedeutet dies im Wesentlichen, dass die Ansicht bereits aktualisiert wird. Sie sollten $apply() in Ihrem Controller wirklich nicht aufrufen müssen. Wenn Ihre Ansicht nicht wie erwartet aktualisiert wird und Sie diesen Fehler nach dem Aufruf von $apply() erhalten, bedeutet dies höchstwahrscheinlich, dass Sie das Modell nicht korrekt aktualisieren. Wenn Sie einige Details veröffentlichen, können wir das Kernproblem herausfinden.

28
dnc253

Die kürzeste Form von sicherem $apply ist:

$timeout(angular.noop)
14
Warlock

Sie können evalAsync auch verwenden. Es wird einige Zeit laufen, nachdem Digest beendet ist!

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

Manchmal erhalten Sie dennoch Fehler, wenn Sie diesen Weg verwenden ( https://stackoverflow.com/a/12859093/801426 ).

Versuche dies:

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

Fixieren Sie es zunächst nicht so

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

Dies ist nicht sinnvoll, da $ phase nur ein boolesches Flag für den $ digest-Zyklus ist. Daher wird Ihr $ apply () manchmal nicht ausgeführt. Und denk dran, es ist eine schlechte Praxis.

Verwenden Sie stattdessen $timeout

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

Wenn Sie Unterstriche oder Lodash verwenden, können Sie defer () verwenden:

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

Sie sollten je nach Kontext $ evalAsync oder $ timeout verwenden.

Dies ist ein Link mit einer guten Erklärung: 

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

5
Luc

Ich würde Ihnen raten, ein benutzerdefiniertes Ereignis zu verwenden, anstatt einen Digest-Zyklus auszulösen.

Ich finde, dass das Senden von benutzerdefinierten Ereignissen und das Registrieren von Listenern für dieses Ereignis eine gute Lösung ist, um eine Aktion auszulösen, die Sie ausführen möchten, unabhängig davon, ob Sie sich in einem Digest-Zyklus befinden oder nicht. 

Durch das Erstellen eines benutzerdefinierten Ereignisses sind Sie auch mit Ihrem Code effizienter, da Sie nur Listener auslösen, die dieses Ereignis abonniert haben, und NICHT alle Überwachungen auslösen, die an den Bereich gebunden sind, als würden Sie den Bereich aufrufen.

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


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

yearofmoo hat beim Erstellen einer wiederverwendbaren $ safeApply-Funktion für uns großartige Arbeit geleistet: 

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

Verwendungszweck :

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

Ich konnte dieses Problem lösen, indem ich $eval anstelle von $apply an Orten aufrief, an denen ich weiß, dass die $digest-Funktion ausgeführt wird.

Gemäß den Anweisungen von docs führt $apply im Wesentlichen Folgendes aus:

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

In meinem Fall ändert ein ng-click eine Variable innerhalb eines Bereichs, und ein $ watch für diese Variable ändert andere Variablen, die $applied sein müssen. Dieser letzte Schritt verursacht den Fehler "Digest bereits läuft".

Durch Ersetzen von $apply durch $eval im Überwachungsausdruck werden die Bereichsvariablen erwartungsgemäß aktualisiert.

Daher ist es erscheint, dass, wenn Digest aufgrund einer anderen Änderung in Angular sowieso ausgeführt wird, $eval 'alles was Sie tun müssen.

2
teleclimber

verwenden Sie stattdessen $scope.$$phase || $scope.$apply();

2

versuchen Sie es mit 

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

anstatt

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

$ applyAsync Planen Sie den Aufruf von $ apply zu einem späteren Zeitpunkt. Dies kann verwendet werden, um mehrere Ausdrücke in eine Warteschlange zu stellen, die in demselben Digest ausgewertet werden müssen.

ANMERKUNG: Innerhalb von $ digest wird $ applyAsync () nur geleert, wenn der aktuelle Bereich $ rootScope ist. Wenn Sie also $ digest für einen untergeordneten Bereich aufrufen, wird die Warteschlange $ applyAsync () nicht implizit geleert.

Beispiel:

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

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

Verweise: 

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

  1. AngularJs Docs
2
Eduardo Eljaiek

Ich habe diese Methode verwendet und es scheint perfekt zu funktionieren. Dies wartet nur auf die Zeit, die der Zyklus beendet ist, und löst dann apply() aus. Rufen Sie einfach die Funktion apply(<your scope>) von einem beliebigen Ort aus auf.

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

Nachdem ich verstanden habe, dass die Angular Dokumente die Prüfung des $$phase und Anti-Pattern aufrufen, habe ich versucht, $timeout und _.defer zum Laufen zu bringen.

Die Timeout- und die Deferred-Methode erzeugen einen Flash mit nicht geparstem {{myVar}} -Inhalt im Dom wie ein FOUT . Für mich war das nicht akzeptabel. Es lässt mich ohne viel dogmatisches zu sagen, dass etwas ein Hack ist und keine passende Alternative hat.

Das einzige, was jedes Mal funktioniert, ist:

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

Ich verstehe die Gefahr dieser Methode nicht oder warum sie von den Leuten in den Kommentaren und dem angular Team als Hack beschrieben wird. Der Befehl scheint präzise und leicht zu lesen:

"Machen Sie die Verdauung, es sei denn, es passiert bereits eine"

In CoffeeScript ist es noch schöner:

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

Was ist das Problem damit? Gibt es eine Alternative, die keinen FOUT schafft? $ safeApply sieht gut aus, verwendet aber auch die Prüfmethode $$phase.

1
SimplGy

Dies ist mein Dienstprogramm:

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

und dies ist ein Beispiel für seine Verwendung:

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

ähnlich den obigen Antworten, aber das hat treu für mich gearbeitet ... in einem Dienst hinzufügen:

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

Sie können verwenden 

$timeout

um den fehler zu verhindern. 

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