Waarom gebruikt if(!$scope.$$phase) $scope.$apply() een antipatroon?

Soms moet ik $scope.$applyin mijn code gebruiken en soms krijg ik een “digest reeds bezig”-foutmelding. Dus ik begon een manier te vinden om dit te omzeilen en vond deze vraag: AngularJS: Voorkom fout $digest die al bezig is bij het aanroepen van $scope.$apply(). Maar in de reacties (en op de hoekige wiki) kun je lezen:

Niet doen als (!$scope.$$phase) $scope.$apply(), dit betekent dat uw $scope.$apply() niet hoog genoeg is in de call-stack.

Dus nu heb ik twee vragen:

  1. Waarom is dit precies een anti-patroon?
  2. Hoe kan ik veilig $scope.$apply gebruiken?

Een andere “oplossing” om te voorkomen dat de fout “digest reeds bezig” wordt gebruikt, lijkt $timeout te gebruiken:

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

Is dat de juiste weg? Is het veiliger? Dus hier is de echte vraag: hoe kan ik geheelde mogelijkheid van een “reeds lopende vertering”-fout elimineren?

PS: ik gebruik alleen $scope.$apply in non-angularjs callbacks die niet synchroon zijn. (voor zover ik weet zijn dit situaties waarin u $scope.$apply moet gebruiken als u wilt dat uw wijzigingen worden toegepast)


Antwoord 1, autoriteit 100%

Na wat meer speurwerk kon ik de vraag oplossen of het altijd veilig is om $scope.$applyte gebruiken. Het korte antwoord is ja.

Lang antwoord:

Vanwege de manier waarop uw browser Javascript uitvoert, is het niet mogelijk dat twee digest-aanroepen toevalligbotsen.

De JavaScript-code die we schrijven, wordt niet allemaal in één keer uitgevoerd, maar wordt om de beurt uitgevoerd. Elk van deze beurten loopt van begin tot eind ononderbroken, en wanneer een beurt loopt, gebeurt er verder niets in onze browser. (van http://jimhoskins.com/2012/12/17/angularjs -and-apply.html)

Daarom kan de fout “digest reeds bezig” alleen in één situatie voorkomen: wanneer een $apply wordt uitgegeven binnen een andere $apply, bijvoorbeeld:

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

Deze situatie kan nietontstaan ​​alswe $scope.apply gebruiken in een pure non-angularjs callback, zoals bijvoorbeeld de callback van setTimeout. Dus de volgende code is 100% kogelvrij en het is geennodig om een ​​if (!$scope.$$phase) $scope.$apply()

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

zelfs deze is veilig:

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

Wat is NIETveilig (omdat $timeout – zoals alle helpers van angularjs – al $scope.$applyvoor u aanroept):

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

Dit verklaart ook waarom het gebruik van if (!$scope.$$phase) $scope.$apply()een anti-patroon is. Je hebt het gewoon niet nodig als je $scope.$applyop de juiste manier gebruikt: in een pure js-callback zoals setTimeoutbijvoorbeeld.

Lees http://jimhoskins.com/2012/12/17 /angularjs-and-apply.htmlvoor de meer gedetailleerde uitleg.


Antwoord 2, autoriteit 15%

Het is nu zeker een anti-patroon. Ik heb een samenvatting zien ontploffen, zelfs als je controleert op de $$-fase. Het is gewoon niet de bedoeling dat je toegang krijgt tot de interne API die wordt aangegeven met $$voorvoegsels.

U zou moeten gebruiken

$scope.$evalAsync();

aangezien dit de voorkeursmethode is in Angular ^1.4 en specifiek wordt weergegeven als een API voor de applicatielaag.


Antwoord 3, autoriteit 8%

In elk geval wanneer uw digest bezig is en u een andere service pusht om te digesteren, geeft het gewoon een fout, d.w.z. digest dat al bezig is.
dus om dit te genezen heb je twee opties.
je kunt controleren op andere lopende samenvattingen, zoals polling.

Eerste

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

als de bovenstaande voorwaarde waar is, kunt u uw $scope.$anders niet toepassen en

tweede oplossingis $timeout gebruiken

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

het laat de andere digest niet starten totdat $timeout zijn uitvoering heeft voltooid.


Antwoord 4, autoriteit 8%

scope.$applyactiveert een $digest-cyclus die fundamenteel is voor 2-way databinding

Een $digest-cyclus controleert op objecten, dwz modellen (om precies te zijn $watch) die zijn gekoppeld aan $scopeom te beoordelen of hun waarden gewijzigd en als het een wijziging detecteert, neemt het de nodige stappen om de weergave bij te werken.

Als je nu $scope.$applygebruikt, krijg je een foutmelding “Al in uitvoering” dus het is vrij duidelijk dat er een $digest actief is, maar wat heeft het veroorzaakt?

ans–> elke aanroep van $http, alle ng-klik, herhaal, toon, verberg enz. triggert een $digest-cyclus EN HET SLECHTSTE DEEL VAN ELKE $SCOPE.

dwz stel dat uw pagina 4 controllers of richtlijnen A,B,C,D heeft

Als je 4 $scopeeigenschappen in elk van deze hebt, dan heb je in totaal 16 $scope eigenschappen op je pagina.

Als je $scope.$applyactiveert in controller D, dan zal een $digestcyclus alle 16 waarden controleren!!! plus alle $rootScope-eigenschappen.

Antwoord–>maar $scope.$digestactiveert een $digestop kind en hetzelfde bereik, zodat er slechts 4 worden gecontroleerd eigenschappen. Dus als je zeker weet dat veranderingen in D geen invloed hebben op A, B, C, gebruik dan $scope.$digest niet $scope.$apply.

Dus een simpele ng-klik of ng-show/hide kan een $digest-cyclus activeren op meer dan 100+ eigenschappen, zelfs als de gebruiker geen enkele gebeurtenis heeft geactiveerd!


Antwoord 5

Gebruik $timeout, dit is de aanbevolen manier.

Mijn scenario is dat ik items op de pagina moet wijzigen op basis van de gegevens die ik van een WebSocket heb ontvangen. En aangezien het buiten Angular is, zonder de $time-out, zal het enige model worden gewijzigd, maar niet de weergave. Omdat Angular niet weet dat dat stukje data is gewijzigd. $timeoutvertelt Angular in feite om de wijziging aan te brengen in de volgende ronde van $digest.

Ik heb het volgende ook geprobeerd en het werkt. Het verschil voor mij is dat $timeout duidelijker is.

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)

Antwoord 6

Ik heb een heel coole oplossing gevonden:

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

injecteer dat waar je nodig hebt:

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

Other episodes