Warum wird if (! $ Scope. $$ phase) $ scope. $ Apply () als Anti-Pattern verwendet?

92

Manchmal muss ich $scope.$applymeinen Code verwenden und manchmal wird der Fehler "Digest bereits in Bearbeitung" ausgegeben. Also fing ich an, einen Weg zu finden und fand diese Frage: AngularJS: Verhindern Sie, dass der Fehler $ Digest bereits ausgeführt wird, wenn Sie $ scope aufrufen. $ Apply () . In den Kommentaren (und im eckigen Wiki) können Sie jedoch lesen:

Tun Sie dies nicht, wenn (! $ Scope. $$ Phase) $ scope. $ Apply () bedeutet, dass Ihr $ scope. $ Apply () im Aufrufstapel nicht hoch genug ist.

Jetzt habe ich zwei Fragen:

  1. Warum genau ist das ein Anti-Muster?
  2. Wie kann ich $ scope. $ Apply sicher verwenden?

Eine andere "Lösung", um den Fehler "Digest bereits in Bearbeitung" zu verhindern, scheint die Verwendung von $ timeout zu sein:

$timeout(function() {
  //...
});

Ist das der richtige Weg? Ist es sicherer? Hier ist also die eigentliche Frage: Wie kann ich die Möglichkeit eines Fehlers "Digest bereits in Bearbeitung" vollständig ausschließen ?

PS: Ich verwende nur $ scope. $ Apply in nicht eckigen Rückrufen, die nicht synchron sind. (Soweit ich weiß, sind dies Situationen, in denen Sie $ scope verwenden müssen. $ apply, wenn Sie möchten, dass Ihre Änderungen angewendet werden.)

Dominik Goltermann
quelle
Aus meiner Erfahrung sollten Sie immer wissen, ob Sie scopevon innerhalb des Winkels oder von außerhalb des Winkels manipulieren . Demnach wissen Sie immer, ob Sie anrufen müssen scope.$applyoder nicht. Und wenn Sie denselben Code sowohl für die Winkel- als auch für die scopeNichtwinkelmanipulation verwenden, machen Sie es falsch. Er sollte immer getrennt sein. Wenn Sie also auf einen Fall stoßen scope.$$phase, in dem Sie überprüfen müssen , ist Ihr Code dies nicht richtig entworfen, und es gibt immer einen Weg, es "richtig" zu machen
doodeec
1
Ich benutze dies nur in nicht eckigen Rückrufen (!) Deshalb bin ich verwirrt
Dominik Goltermann
2
Wenn es nicht eckig wäre, würde es keinen digest already in progressFehler
auslösen
1
Das ist was ich dachte. Die Sache ist: Es wirft nicht immer den Fehler. Nur ab und zu. Mein Verdacht ist, dass die Anwendung zufällig mit einem anderen Digest kollidiert. Ist das möglich?
Dominik Goltermann
Ich denke nicht, dass das möglich ist, wenn der Rückruf streng nicht eckig ist
doodeec

Antworten:

113

Nach einigem Graben konnte ich die Frage lösen, ob es immer sicher ist, es zu benutzen $scope.$apply. Die kurze Antwort lautet ja.

Lange Antwort:

Aufgrund der Ausführung von Javascript durch Ihren Browser ist es nicht möglich, dass zwei Digest-Aufrufe zufällig kollidieren .

Der von uns geschriebene JavaScript-Code wird nicht alle auf einmal ausgeführt, sondern abwechselnd ausgeführt. Jede dieser Runden läuft von Anfang bis Ende ununterbrochen, und wenn eine Runde läuft, passiert in unserem Browser nichts anderes. (von http://jimhoskins.com/2012/12/17/angularjs-and-apply.html )

Daher kann der Fehler "Digest bereits in Bearbeitung" nur in einer Situation auftreten: Wenn ein $ apply in einem anderen $ apply ausgegeben wird, z.

$scope.apply(function() {
  // some code...
  $scope.apply(function() { ... });
});

Diese Situation kann nicht entstehen, wenn wir $ scope.apply in einem reinen Rückruf ohne Winkel verwenden, wie zum Beispiel dem Rückruf von setTimeout. Der folgende Code ist also 100% kugelsicher und es ist nicht erforderlich, aif (!$scope.$$phase) $scope.$apply()

setTimeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

Auch dieser ist sicher:

$scope.$apply(function () {
    setTimeout(function () {
        $scope.$apply(function () {
            $scope.message = "Timeout called!";
        });
    }, 2000);
});

Was ist NICHT sicher (weil $ timeout - wie alle Angularjs-Helfer - bereits $scope.$applynach Ihnen verlangt ):

$timeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

Dies erklärt auch, warum die Verwendung von if (!$scope.$$phase) $scope.$apply()ein Anti-Muster ist. Sie brauchen es einfach nicht, wenn Sie es $scope.$applyrichtig verwenden: In einem reinen js-Rückruf wie setTimeoutzum Beispiel.

Weitere Informationen finden Sie unter http://jimhoskins.com/2012/12/17/angularjs-and-apply.html .

Dominik Goltermann
quelle
Ich habe ein Beispiel, in dem ich einen Service erstelle, mit dem $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });ich wirklich nicht weiß, warum ich $ hier anwenden muss, weil ich $ document.bind verwende.
Betty St
weil $ document nur "Ein jQuery- oder jqLite-Wrapper für das window.document-Objekt des Browsers ist." und wie folgt implementiert: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }Es gibt dort keine Anwendung.
Dominik Goltermann
11
$timeoutSemantisch bedeutet, Code nach einer Verzögerung auszuführen. Es mag eine funktionssichere Sache sein, aber es ist ein Hack. Es sollte eine sichere Möglichkeit geben, $ apply zu verwenden, wenn Sie nicht wissen können, ob ein $digestZyklus ausgeführt wird oder Sie sich bereits in einem befinden $apply.
John Strickler
1
Ein weiterer Grund, warum es schlecht ist: Es verwendet interne Variablen ($$ -Phase), die nicht Teil der öffentlichen API sind, und sie können in einer neueren Version von Angular geändert werden und somit Ihren Code beschädigen. Ihr Problem mit der Auslösung synchroner Ereignisse ist jedoch interessant
Dominik Goltermann
4
Ein neuerer Ansatz ist die Verwendung von $ scope. $ EvalAsync (), das nach Möglichkeit sicher im aktuellen Digest-Zyklus oder im nächsten Zyklus ausgeführt wird. Siehe bennadel.com/blog/…
jaymjarri
16

Es ist jetzt definitiv ein Anti-Muster. Ich habe gesehen, wie eine Verdauung explodierte, selbst wenn Sie nach der $$ -Phase suchen. Sie sollten einfach nicht auf die interne API zugreifen, die durch $$Präfixe gekennzeichnet ist.

Du solltest benutzen

 $scope.$evalAsync();

da dies die bevorzugte Methode in Angular ^ 1.4 ist und speziell als API für die Anwendungsschicht verfügbar gemacht wird.

FlavourScape
quelle
9

In jedem Fall, wenn Ihr Digest in Bearbeitung ist und Sie einen anderen Dienst zum Digest drängen, wird einfach ein Fehler ausgegeben, dh der Digest wird bereits ausgeführt. Um dies zu heilen, haben Sie zwei Möglichkeiten. Sie können nach anderen laufenden Digests wie Polling suchen.

Erster

if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') {
    $scope.$apply();
}

Wenn die obige Bedingung erfüllt ist, können Sie Ihren $ scope anwenden. $ andernfalls nicht anwenden und

Die zweite Lösung ist die Verwendung von $ timeout

$timeout(function() {
  //...
})

Der andere Digest wird erst gestartet, wenn $ timeout seine Ausführung abgeschlossen hat.

Lalit Sachdeva
quelle
1
herabgestimmt; In der Frage wird speziell gefragt, warum Sie das, was Sie hier beschreiben, NICHT tun sollen, und nicht, wie Sie es auf andere Weise umgehen können. In der ausgezeichneten Antwort von @gaul finden Sie Informationen zum Verwendungszweck $scope.$apply();.
PureSpider
Obwohl die Frage nicht beantwortet wird: $timeoutist der Schlüssel! es funktioniert und später fand ich, dass es auch empfohlen wird.
Himel Nag Rana
Ich weiß, dass es ziemlich spät ist, dies 2 Jahre später zu kommentieren, aber seien Sie vorsichtig, wenn Sie $ timeout zu viel verwenden, da dies Sie zu viel Leistung kosten kann, wenn Sie keine gute Anwendungsstruktur haben
cpoDesign
9

scope.$apply löst a aus $digest Zyklus aus, der für die bidirektionale Datenbindung von grundlegender Bedeutung ist

Ein $digestZyklus prüft , ob Objekte (dh Modelle) (um genau zu sein $watch) angehängt sind, $scopeum festzustellen, ob sich ihre Werte geändert haben. Wenn eine Änderung festgestellt wird, werden die erforderlichen Schritte ausgeführt, um die Ansicht zu aktualisieren.

Wenn Sie jetzt verwenden $scope.$apply, wird der Fehler "Bereits in Bearbeitung" angezeigt. Es ist also ziemlich offensichtlich, dass ein $ Digest ausgeführt wird, aber was hat ihn ausgelöst?

ans -> alle $httpAnrufe, alle ng-click, wiederholen, zeigen, verstecken usw. lösen einen $digestZyklus aus UND DER SCHLECHTESTE TEIL LÄUFT JEDEN GELTUNGSBEREICH.

Angenommen, Ihre Seite verfügt über 4 Controller oder Anweisungen A, B, C, D.

Wenn Sie jeweils 4 $scopeEigenschaften haben, befinden sich auf Ihrer Seite insgesamt 16 $ scope-Eigenschaften.

Wenn Sie $scope.$applyin Controller D auslösen, $digestprüft ein Zyklus alle 16 Werte !!! plus alle $ rootScope-Eigenschaften.

Antwort -> aber $scope.$digestAuslöser ein $digestauf Kind und gleichen Umfang so wird es nur 4 Objekte überprüfen. Wenn Sie also sicher sind, dass Änderungen in D A, B, C nicht beeinflussen, verwenden Sie $scope.$digest not $scope.$apply.

Ein bloßes Klicken oder Ausblenden / Ausblenden kann also einen $digestZyklus für mehr als 100 Eigenschaften auslösen, selbst wenn der Benutzer kein Ereignis ausgelöst hat !

Rishul Matta
quelle
2
Ja, das wurde mir leider erst spät im Projekt klar. Ich hätte Angular nicht benutzt, wenn ich das von Anfang an gewusst hätte. Alle Standardanweisungen lösen einen $ scope. $ Apply aus, der wiederum $ rootScope. $ Digest aufruft und schmutzige Prüfungen für ALLE Bereiche durchführt. Schlechte Designentscheidung, wenn Sie mich fragen. Ich sollte die Kontrolle darüber haben, welche Bereiche schmutzig überprüft werden sollten, da ich weiß, wie die Daten mit diesen Bereichen verknüpft sind!
MoonStom
0

Verwenden $timeout Sie es auf die empfohlene Weise.

Mein Szenario ist, dass ich Elemente auf der Seite basierend auf den Daten ändern muss, die ich von einem WebSocket erhalten habe. Und da es sich außerhalb von Angular befindet, ohne das $ timeout, wird das einzige Modell geändert, nicht jedoch die Ansicht. Weil Angular nicht weiß, dass diese Daten geändert wurden.$timeoutsagt Angular im Grunde, er solle die Änderung in der nächsten Runde von $ Digest vornehmen.

Ich habe auch folgendes versucht und es funktioniert. Der Unterschied für mich ist, dass $ timeout klarer ist.

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)
James J. Ye
quelle
Es ist viel sauberer, Ihren Socket-Code in $ apply zu verpacken (ähnlich wie Angulars auf AJAX-Code, dh $http). Andernfalls müssen Sie diesen Code überall wiederholen.
Timruffles
Dies wird definitiv nicht empfohlen. Außerdem wird gelegentlich eine Fehlermeldung angezeigt, wenn $ scope die Phase $$ hat. Verwenden Sie stattdessen $ scope. $ evalAsync ();
FlavourScape
Es ist nicht nötig, $scope.$applywenn Sie setTimeoutoder$timeout
Kunal
-1

Ich fand eine sehr coole Lösung:

.factory('safeApply', [function($rootScope) {
    return function($scope, fn) {
        var phase = $scope.$root.$$phase;
        if (phase == '$apply' || phase == '$digest') {
            if (fn) {
                $scope.$eval(fn);
            }
        } else {
            if (fn) {
                $scope.$apply(fn);
            } else {
                $scope.$apply();
            }
        }
    }
}])

injizieren Sie das, wo Sie brauchen:

.controller('MyCtrl', ['$scope', 'safeApply',
    function($scope, safeApply) {
        safeApply($scope); // no function passed in
        safeApply($scope, function() { // passing a function in
        });
    }
])
bora89
quelle