Dlaczego używa się if (! $ Scope. $$ phase) $ scope. $ Apply () jako anty-wzorzec?

92

Czasami muszę użyć $scope.$applykodu w moim kodzie, a czasami wyświetla błąd „podsumowanie już w toku”. Zacząłem więc szukać sposobu obejścia tego problemu i znalazłem to pytanie: AngularJS: Zapobiegaj błędom $ Digest już w toku podczas wywoływania $ scope. $ Apply () . Jednak w komentarzach (i na kątowej wiki) możesz przeczytać:

Nie rób tego, jeśli (! $ Scope. $$ phase) $ scope. $ Apply (), oznacza to, że twój $ scope. $ Apply () nie jest wystarczająco wysoko na stosie wywołań.

Więc teraz mam dwa pytania:

  1. Dlaczego dokładnie jest to anty-wzór?
  2. Jak bezpiecznie używać $ scope. $ Apply?

Wydaje się, że innym „rozwiązaniem” błędu „podsumowanie już w toku” jest użycie $ timeout:

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

Czy to jest droga? Czy to jest bezpieczniejsze? Oto więc prawdziwe pytanie: jak mogę całkowicie wyeliminować możliwość wystąpienia błędu „podsumowanie już w toku”?

PS: Używam tylko $ scope. $ Stosuje się w wywołaniach zwrotnych innych niż angularjs, które nie są synchroniczne. (o ile wiem, są to sytuacje, w których musisz użyć $ scope. $ zastosuj, jeśli chcesz, aby zmiany zostały zastosowane)

Dominik Goltermann
źródło
Z mojego doświadczenia, zawsze powinieneś wiedzieć, czy manipulujesz scopeod wewnątrz, czy z zewnątrz kątowo. Dlatego zawsze wiesz, czy musisz zadzwonić, scope.$applyczy nie. A jeśli używasz tego samego kodu zarówno do scopemanipulacji kątowych , jak i nie-kątowych , robisz to źle, zawsze powinien być oddzielony ... więc w zasadzie, jeśli napotkasz przypadek, w którym musisz sprawdzić scope.$$phase, twój kod nie jest zaprojektowane w prawidłowy sposób i zawsze jest sposób, aby to zrobić
``
1
używam tego tylko w nie-kątowych wywołaniach zwrotnych (!) Dlatego jestem zdezorientowany
Dominik Goltermann
2
gdyby nie był kątowy, nie digest already in progress
generowałby
1
tak myślałem. Rzecz w tym, że nie zawsze powoduje to błąd. Tylko raz na jakiś czas. Podejrzewam, że aplikacja przypadkowo koliduje z innym podsumowaniem. Czy to jest możliwe?
Dominik Goltermann
Nie sądzę, żeby było to możliwe, jeśli wywołanie zwrotne jest ściśle nie-kątowe
doodeec

Odpowiedzi:

113

Po dłuższych poszukiwaniach udało mi się rozwiązać pytanie, czy zawsze można go bezpiecznie używać $scope.$apply. Krótka odpowiedź brzmi: tak.

Długa odpowiedź:

Ze względu na to, jak Twoja przeglądarka wykonuje Javascript, nie jest możliwe, że dwa strawienia rozmowy zderzają się przypadkowo .

Kod JavaScript, który piszemy, nie działa w całości za jednym razem, zamiast tego jest wykonywany po kolei. Każdy z tych zakrętów przebiega nieprzerwanie od początku do końca, a kiedy tura jest w toku, nic więcej się nie dzieje w naszej przeglądarce. (z http://jimhoskins.com/2012/12/17/angularjs-and-apply.html )

W związku z tym błąd „podsumowanie już w toku” może wystąpić tylko w jednej sytuacji: gdy $ Apply jest wystawiane w innym $ Apply, np .:

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

Taka sytuacja nie może wystąpić, jeśli użyjemy $ scope.apply w czystym wywołaniu zwrotnym innym niż angularjs, takim jak na przykład wywołanie zwrotne z setTimeout. Więc poniższy kod jest w 100% kuloodporny i nie ma potrzeby wykonywania plikuif (!$scope.$$phase) $scope.$apply()

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

nawet ten jest bezpieczny:

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

Co NIE jest bezpieczne (ponieważ $ timeout - podobnie jak wszyscy pomocnicy angularjs - już $scope.$applycię wzywa ):

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

To wyjaśnia również, dlaczego użycie if (!$scope.$$phase) $scope.$apply()jest anty-wzorcem. Po prostu nie potrzebujesz go, jeśli używasz $scope.$applywe właściwy sposób: w czystym wywołaniu zwrotnym js, takim jak setTimeoutna przykład.

Przeczytaj http://jimhoskins.com/2012/12/17/angularjs-and-apply.html, aby uzyskać bardziej szczegółowe wyjaśnienie.

Dominik Goltermann
źródło
Mam przykład, w którym tworzę usługę z $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });naprawdę nie wiem, dlaczego muszę tutaj złożyć wniosek o $, ponieważ używam $ document.bind ..
Betty St
ponieważ $ document jest tylko „opakowaniem jQuery lub jqLite dla obiektu window.document przeglądarki”. i realizowane w następujący sposób: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }Nie ma tam zastosowania.
Dominik Goltermann
11
$timeoutsemantycznie oznacza uruchamianie kodu po opóźnieniu. Może to być funkcjonalnie bezpieczne, ale jest to hack. Powinien istnieć bezpieczny sposób korzystania z $ apply, gdy nie możesz wiedzieć, czy $digestcykl jest w toku lub czy jesteś już w środku $apply.
John Strickler
1
inny powód, dla którego jest zły: używa zmiennych wewnętrznych (faza $$), które nie są częścią publicznego interfejsu API i mogą zostać zmienione w nowszej wersji angulara, a tym samym zepsuć kod. Twój problem z synchronicznym wyzwalaniem zdarzeń jest jednak interesujący
Dominik Goltermann
4
Nowszym podejściem jest użycie $ scope. $ EvalAsync (), która jest bezpiecznie wykonywana w bieżącym cyklu podsumowania, jeśli to możliwe, lub w następnym cyklu. Zobacz bennadel.com/blog/…
jaymjarri
16

Obecnie jest to zdecydowanie anty-wzór. Widziałem podsumowanie, nawet jeśli sprawdzisz fazę $$. Po prostu nie powinieneś mieć dostępu do wewnętrznego interfejsu API oznaczonego $$prefiksami.

Powinieneś użyć

 $scope.$evalAsync();

ponieważ jest to preferowana metoda w Angular ^ 1.4 i jest specjalnie udostępniona jako API dla warstwy aplikacji.

FlavorScape
źródło
9

W każdym przypadku, gdy twoje podsumowanie jest w toku i naciskasz na inną usługę, po prostu wyświetla błąd, tj. Przegląd już trwa. więc aby to wyleczyć, masz dwie możliwości. możesz sprawdzić inne trwające podsumowanie, na przykład odpytywanie.

Pierwszy

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

jeśli powyższy warunek jest spełniony, możesz zastosować swój $ scope. $ zastosuj w inny sposób nie i

Drugim rozwiązaniem jest użycie $ timeout

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

nie pozwoli na rozpoczęcie drugiego skrótu, dopóki $ timeout nie zakończy jego wykonania.

Lalit Sachdeva
źródło
1
odrzucony; Pytanie konkretnie dotyczy tego, dlaczego NIE robić tego, co tutaj opisujesz, a nie innego sposobu, aby to obejść. Zobacz doskonałą odpowiedź @gaul, aby dowiedzieć się, kiedy używać $scope.$apply();.
PureSpider
Choć nie odpowiadając na pytanie: $timeoutjest kluczem! to działa, a później okazało się, że jest również zalecane.
Himel Nag Rana
Wiem, że jest już dość późno, aby dodać komentarz do tego 2 lata później, ale bądź ostrożny, jeśli używasz $ timeout za dużo, ponieważ może to kosztować Cię zbyt dużo wydajności, jeśli nie masz dobrej struktury aplikacji
cpoDesign
9

scope.$applywyzwala $digestcykl, który jest podstawą dwukierunkowego wiązania danych

ZA $digest sprawdza rowerowe dla obiektów tj modeli (dokładniej $watch) dołączonych do $scopeoceny, czy ich wartości uległy zmianie i jeżeli wykryje zmianę wtedy podejmuje niezbędne kroki, aby zaktualizować widok.

Teraz, gdy używasz $scope.$apply, napotykasz błąd „Już w toku”, więc jest całkiem oczywiste, że podsumowanie $ działa, ale co go spowodowało?

ans -> co $http połączenie, wszystkie ng-klik, powtórz, pokaż, ukryj itp. uruchamiają $digestcykl I NAJGORSZA CZĘŚĆ DZIAŁA KAŻDEGO ZAKRESU

tzn. powiedz, że twoja strona ma 4 kontrolery lub dyrektywy A, B, C, D

Jeśli masz 4 $scope właściwości w każdej z nich, na swojej stronie masz łącznie 16 właściwości zakresu $.

Jeśli wyzwolisz $scope.$apply w kontrolerze D, $digestcykl sprawdzi wszystkie 16 wartości !!! plus wszystkie właściwości $ rootScope.

Odpowiedź -> ale $scope.$digestwyzwala $digeston element podrzędny i ten sam zakres, więc sprawdzi tylko 4 właściwości. Więc jeśli jesteś pewien, że zmiany w D nie wpłyną na A, B, C, użyj$scope.$diges t nie $scope.$apply.

Zatem zwykłe naciśnięcie klawisza ng-click lub ng-show / hide może wywołać $digestcykl w ponad 100+ właściwościach, nawet jeśli użytkownik nie uruchomił żadnego zdarzenia !

Rishul Matta
źródło
2
Tak, niestety zdałem sobie sprawę z tego późnego etapu projektu. Nie użyłbym Angulara, gdybym wiedział to od początku. Wszystkie dyrektywy standardowe uruchamiają $ scope. $ Apply, który z kolei wywołuje $ rootScope. $ Digest, który wykonuje brudne sprawdzenia na WSZYSTKICH zakresach. Zła decyzja projektowa, jeśli o mnie chodzi. Powinienem mieć kontrolę nad tym, które zakresy powinny być brudne, ponieważ WIEM, JAK DANE SĄ ZWIĄZANE Z TYMI ZAKRESAMI!
MoonStom
0

Użyj $timeout, jest to sposób zalecany.

Mój scenariusz jest taki, że muszę zmienić elementy na stronie na podstawie danych, które otrzymałem z WebSocket. A ponieważ jest poza Angularem, bez limitu czasu $ timeout, jedyny model zostanie zmieniony, ale nie widok. Ponieważ Angular nie wie, że część danych została zmieniona. $timeoutpo prostu mówi Angularowi, aby dokonał zmiany w następnej rundzie podsumowania $.

Wypróbowałem również następujące i działa. Dla mnie różnica polega na tym, że $ timeout jest wyraźniejszy.

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)
James J. Ye
źródło
O wiele czystsze jest zawijanie kodu gniazda w $ apply (podobnie jak w przypadku Angulara w kodzie AJAX, tj $http.). W przeciwnym razie będziesz musiał powtarzać ten kod w każdym miejscu.
Timruffles
to zdecydowanie nie jest zalecane. Ponadto od czasu do czasu pojawi się błąd podczas wykonywania tej czynności, jeśli $ scope ma fazę $$. zamiast tego powinieneś użyć $ scope. $ evalAsync ();
FlavorScape
Nie ma potrzeby, $scope.$applyczy używasz setTimeoutlub$timeout
Kunal
-1

Znalazłem bardzo fajne rozwiązanie:

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

wstrzyknij tam, gdzie potrzebujesz:

.controller('MyCtrl', ['$scope', 'safeApply',
    function($scope, safeApply) {
        safeApply($scope); // no function passed in
        safeApply($scope, function() { // passing a function in
        });
    }
])
bora89
źródło