AngularJS : Voorkom fout $digest die al bezig is bij het aanroepen van $scope.$apply()

Ik merk dat ik mijn pagina steeds vaker handmatig moet bijwerken naar mijn bereik sinds ik een toepassing in hoekig heb gebouwd.

De enige manier die ik ken om dit te doen, is door $apply()aan te roepen vanuit het bereik van mijn controllers en richtlijnen. Het probleem hiermee is dat het steeds een foutmelding naar de console blijft geven die luidt:

Fout: $digest is al bezig

Weet iemand hoe deze fout kan worden vermeden of hetzelfde kan worden bereikt, maar op een andere manier?


Antwoord 1, autoriteit 100%

Uit een recente discussie met de Angular-jongens over dit onderwerp: Om toekomstbestendige redenen mag u $$phase

niet gebruiken

Als erop wordt gedrukt voor de “juiste” manier om het te doen, is het antwoord momenteel

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

Ik kwam dit onlangs tegen bij het schrijven van hoekige services om de Facebook-, Google- en Twitter-API’s in te pakken die, in verschillende mate, callbacks hebben ingeleverd.

Hier is een voorbeeld vanuit een service. (Omwille van de beknoptheid is de rest van de service — die variabelen instelde, $timeout enz. injecteerde — weggelaten.)

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

Houd er rekening mee dat het vertragingsargument voor $timeout optioneel is en standaard op 0 staat als het niet is ingesteld ($timeoutroept $browser.deferdie standaard 0 als vertraging niet is ingesteld)

Een beetje niet-intuïtief, maar dat is het antwoord van de jongens die Angular schrijven, dus het is goed genoeg voor mij!


Antwoord 2, autoriteit 99%

Gebruik dit patroon niet– Dit zal uiteindelijk meer fouten veroorzaken dan het oplost. Ook al denk je dat het iets heeft opgelost, dat is niet het geval.

Je kunt controleren of een $digestal bezig is door $scope.$$phaseaan te vinken.

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

$scope.$$phaseretourneert "$digest"of "$apply"als een $digestof $applyis bezig. Ik geloof dat het verschil tussen deze toestanden is dat $digestde horloges van de huidige scope en zijn kinderen zal verwerken, en $applyde watchers van alle scopes zal verwerken.

Om het punt van @dnc253 te zeggen: als je merkt dat je vaak $digestof $applybelt, doe je het misschien verkeerd. Over het algemeen merk ik dat ik moet verwerken wanneer ik de status van de scope moet bijwerken als gevolg van een DOM-gebeurtenis die buiten het bereik van Angular wordt geactiveerd. Bijvoorbeeld wanneer een twitter-bootstrap-modal verborgen wordt. Soms wordt de DOM-gebeurtenis geactiveerd wanneer een $digestaan de gang is, soms niet. Daarom gebruik ik deze cheque.

Ik zou graag een betere manier willen weten als iemand er een weet.


Uit opmerkingen:
door @anddoutoi

angular.js Anti Patterns

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

Antwoord 3, autoriteit 49%

De samenvattingscyclus is een synchrone aanroep. Het geeft geen controle aan de gebeurtenislus van de browser totdat het klaar is. Er zijn een paar manieren om hiermee om te gaan. De gemakkelijkste manier om hiermee om te gaan, is door de ingebouwde $timeout te gebruiken, en een tweede manier is als je underscore of lodash gebruikt (en dat zou je ook moeten doen), bel het volgende:

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

of als je lodash hebt:

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

We hebben verschillende tijdelijke oplossingen geprobeerd en we hadden een hekel aan het injecteren van $rootScope in al onze controllers, richtlijnen en zelfs sommige fabrieken. Dus de $timeout en _.defer zijn tot nu toe onze favoriet. Deze methoden vertellen angular met succes te wachten tot de volgende animatielus, wat garandeert dat de huidige scope.$apply voorbij is.


Antwoord 4, autoriteit 40%

Veel van de antwoorden hier bevatten goede adviezen, maar kunnen ook tot verwarring leiden. Gewoon $timeoutgebruiken is nietde beste en ook niet de juiste oplossing.
Lees dat ook als u zich zorgen maakt over prestaties of schaalbaarheid.

Dingen die je moet weten

  • $$phaseis privé voor het framework en daar zijn goede redenen voor.

  • $timeout(callback)wacht tot de huidige samenvattingscyclus (indien aanwezig) is voltooid, voert dan de callback uit en voert aan het einde een volledige $apply.

  • $timeout(callback, delay, false)zal hetzelfde doen (met een optionele vertraging voordat de callback wordt uitgevoerd), maar zal geen $apply(derde argument) dat prestaties bespaart als je je Angular-model ($scope) niet hebt gewijzigd.

  • $scope.$apply(callback)roept onder andere $rootScope.$digestaan, wat betekent dat het de rootscope van de applicatie en al zijn kinderen, zelfs als u zich binnen een geïsoleerd bereik bevindt.

  • $scope.$digest()synchroniseert eenvoudig het model met de weergave, maar verteert het bereik van de ouders niet, wat veel uitvoeringen kan besparen bij het werken aan een geïsoleerd onderdeel van uw HTML met een geïsoleerd bereik (meestal van een richtlijn). $digest neemt geen callback aan: je voert de code uit en vervolgens digest.

  • $scope.$evalAsync(callback)is geïntroduceerd met angularjs 1.2 en zal waarschijnlijk de meeste van je problemen oplossen. Raadpleeg de laatste paragraaf voor meer informatie.

  • als u de fout $digest already in progress errorkrijgt, dan is uw architectuur verkeerd: ofwel hoeft u uw bereik niet opnieuw te verwerken, of u zou niet in verantwoordelijk voor dat(zie hieronder).

Hoe u uw code structureert

Als je die foutmelding krijgt, probeer je je scope te verwerken terwijl deze al bezig is: aangezien je de status van je scope op dat moment niet kent, ben je niet verantwoordelijk voor het afhandelen van de vertering.

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

En als je weet wat je doet en aan een geïsoleerde kleine richtlijn werkt terwijl het deel uitmaakt van een grote Angular-applicatie, zou je $digest kunnen gebruiken in plaats van $apply om prestaties op te slaan.

Update sinds Angularjs 1.2

Er is een nieuwe, krachtige methode toegevoegd aan elke $scope: $evalAsync. In principe zal het zijn callback uitvoeren binnen de huidige samenvattingscyclus als er een plaatsvindt, anders zal een nieuwe samenvattingscyclus beginnen met het uitvoeren van de callback.

Dat is nog steeds niet zo goed als een $scope.$digestals je echt weet dat je alleen een geïsoleerd deel van je HTML hoeft te synchroniseren (sinds een nieuwe $applywordt geactiveerd als er geen actief is), maar dit is de beste oplossing wanneer u een functie uitvoert waarvan u niet kunt weten of deze synchroon zal worden uitgevoerd of niet, bijvoorbeeld na het ophalen van een bron mogelijk in de cache: soms is hiervoor een asynchrone aanroep naar een server vereist, anders wordt de bron synchroon lokaal opgehaald.

In deze gevallen en alle andere gevallen waarin u een !$scope.$$phasehad, gebruik dan $scope.$evalAsync( callback )


Antwoord 5, autoriteit 13%

Handige kleine hulpmethode om dit proces DROOG te houden:

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

Antwoord 6, autoriteit 5%

Ik had hetzelfde probleem met scripts van derden, zoals CodeMirror en Krpano,
en zelfs het gebruik van safeApply-methoden die hier worden genoemd, hebben de fout voor mij niet opgelost.

Maar wat het wel heeft opgelost, is het gebruik van de $timeout-service (vergeet niet om het eerst te injecteren).

Dus zoiets als:

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

en als je binnen je code gebruik maakt van

dit

misschien omdat het in de controller van een fabrieksrichtlijn zit of gewoon een soort binding nodig heeft, dan zou je zoiets doen als:

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

Antwoord 7, autoriteit 5%

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

Het probleem doet zich voor wanneer u $applyaanroept die soms asynchroon wordt uitgevoerd buiten Angular-code (wanneer $apply zou moeten worden gebruikt) en soms synchroon binnen Angular-code (waardoor de $digest already in progressfout).

Dit kan bijvoorbeeld gebeuren wanneer u een bibliotheek heeft die items asynchroon ophaalt van een server en deze in de cache opslaat. De eerste keer dat een item wordt opgevraagd, wordt het asynchroon opgehaald om de uitvoering van de code niet te blokkeren. De tweede keer is het item echter al in de cache, zodat het synchroon kan worden opgehaald.

De manier om deze fout te voorkomen is ervoor te zorgen dat de code die $applyaanroept, asynchroon wordt uitgevoerd. Dit kan gedaan worden door uw code uit te voeren in een aanroep naar $timeoutmet de vertraging ingesteld op 0(wat de standaardinstelling is). Als u uw code echter binnen $timeoutaanroept, hoeft u $applyniet aan te roepen, omdat $timeout vanzelf weer een $digest-cyclus activeert, die op zijn beurt alle noodzakelijke updates zal doen, enz.

Oplossing

Kortom, in plaats van dit te doen:

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

doe dit:

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

Bel alleen $applyaan als u weet dat de code wordt uitgevoerd, deze wordt altijd buiten Angular-code uitgevoerd (uw oproep naar $apply vindt bijvoorbeeld plaats binnen een callback die wordt aangeroepen door code buiten uw Angular-code code).

Tenzij iemand zich bewust is van een belangrijk nadeel van het gebruik van $timeoutboven $apply, zie ik niet in waarom je niet altijd $timeout(zonder vertraging) in plaats van $apply, omdat het ongeveer hetzelfde zal doen.


Antwoord 8, autoriteit 4%

Als u deze foutmelding krijgt, betekent dit in feite dat uw weergave al wordt bijgewerkt. Het zou echt niet nodig moeten zijn om $apply()aan te roepen in je controller. Als uw weergave niet wordt bijgewerkt zoals u zou verwachten, en u krijgt deze foutmelding nadat u $apply()heeft aangeroepen, betekent dit hoogstwaarschijnlijk dat u het model niet correct bijwerkt. Als je wat details plaatst, kunnen we het kernprobleem achterhalen.


Antwoord 9, autoriteit 2%

De kortste vorm van veilige $applyis:

$timeout(angular.noop)

Antwoord 10, autoriteit 2%

U kunt ook evalAsync gebruiken. Het wordt uitgevoerd enige tijd nadat de digest is voltooid!

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

Antwoord 11

Allereerst, los het niet op deze manier op

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

Het is niet logisch omdat $phase slechts een booleaanse vlag is voor de $digest-cyclus, dus uw $apply() wordt soms niet uitgevoerd. En onthoud dat het een slechte gewoonte is.

Gebruik in plaats daarvan $timeout

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

Als u underscore of lodash gebruikt, kunt u defer():

gebruiken

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

Antwoord 12

Soms krijg je nog steeds fouten als je deze manier gebruikt (https://stackoverflow.com/a/12859093/801426https://stackoverflow.com/a/12859093/801426).

Probeer dit:

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

Antwoord 13

U moet $evalAsync of $timeout gebruiken, afhankelijk van de context.

Dit is een link met een goede uitleg:

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


Antwoord 14

probeer

. te gebruiken

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

in plaats van

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

$applyAsync Plan het aanroepen van $apply op een later tijdstip. Dit kan worden gebruikt om meerdere uitdrukkingen in de wachtrij te plaatsen die in dezelfde samenvatting moeten worden geëvalueerd.

OPMERKING: binnen $digest wordt $applyAsync() alleen leeggemaakt als het huidige bereik $rootScope is. Dit betekent dat als u $digest aanroept op een onderliggend bereik, het niet impliciet de wachtrij $applyAsync() leegmaakt.

Voorbeeld:

 $scope.$applyAsync(function () {
                if (!authService.authenticated) {
                    return;
                }
                if (vm.file !== null) {
                    loadService.setState(SignWizardStates.SIGN);
                } else {
                    loadService.setState(SignWizardStates.UPLOAD_FILE);
                }
            });

Referenties:

1.Scope.$applyAsync() versus Scope.$evalAsync() in AngularJS 1.3

  1. AngularJs-documenten

Antwoord 15

Ik raad je aan om een ​​aangepaste gebeurtenis te gebruiken in plaats van een samenvattingscyclus te starten.

Ik ben erachter gekomen dat het uitzenden van aangepaste gebeurtenissen en het registreren van luisteraars voor deze gebeurtenissen een goede oplossing is voor het activeren van een actie die u wilt laten plaatsvinden, ongeacht of u zich in een samenvattingscyclus bevindt of niet.

Door een aangepaste gebeurtenis te maken, gaat u ook efficiënter om met uw code, omdat u alleen luisteraars activeert die op die gebeurtenis zijn geabonneerd en NIET alle horloges activeert die aan het bereik zijn gebonden, zoals u zou doen als u scope.$apply zou aanroepen.

>

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

Antwoord 16

yearofmoo heeft geweldig werk geleverd door een herbruikbare $safeApply-functie voor ons te maken:

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

Gebruik:

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

Antwoord 17

Ik heb dit probleem kunnen oplossen door $evalaan te roepen in plaats van $applyop plaatsen waarvan ik weet dat de functie $digestzal worden uitgevoerd.

Volgens de docsdoet $applyin feite dit:

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

In mijn geval verandert een ng-clickeen variabele binnen een bereik, en een $watch op die variabele verandert andere variabelen die $appliedmoeten zijn. Deze laatste stap veroorzaakt de fout “digest reeds bezig”.

Door $applyte vervangen door $evalin de watch-expressie worden de bereikvariabelen zoals verwacht bijgewerkt.

Daarom lijkt hetdat als digest toch wordt uitgevoerd vanwege een andere wijziging binnen Angular, $eval‘ing alles is wat u hoeft te doen.


Antwoord 18

gebruik $scope.$$phase || $scope.$apply();in plaats daarvan


Antwoord 19

Begrijpend dat de Angular-documenten het controleren van de $$phaseeen anti-patroon, ik heb geprobeerd om $timeouten _.deferte laten werken.

De time-out en uitgestelde methoden creëren een flits van niet-geparseerde {{myVar}}inhoud in de dom, zoals een FOUT. Voor mij was dit niet acceptabel. Het laat me niet veel om dogmatisch te worden verteld dat iets een hack is en dat ik geen geschikt alternatief heb.

Het enige dat elke keer werkt is:

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

Ik begrijp het gevaar van deze methode niet, of waarom het door mensen in de commentaren en het hoekige team wordt beschreven als een hack. De opdracht lijkt nauwkeurig en gemakkelijk te lezen:

“Doe de samenvatting tenzij er al een plaatsvindt”

In CoffeeScript is het nog mooier:

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

Wat is hier het probleem mee? Is er een alternatief dat geen FOUT creëert? $safeApplyziet er goed uit, maar gebruikt de inspectiemethode $$phase, ook.


Antwoord 20

Dit is mijn utils-service:

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

en dit is een voorbeeld voor het gebruik ervan:

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

Antwoord 21

Ik heb deze methode gebruikt en het lijkt prima te werken. Dit wacht gewoon op de tijd dat de cyclus is afgelopen en activeert vervolgens apply(). Roep eenvoudig de functie apply(<your scope>)aan vanaf waar u maar wilt.

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

Antwoord 22

Toen ik debugger uitschakelde, treedt de fout niet meer op. In mijn gevalwas het omdat debugger de uitvoering van de code stopte.


Antwoord 23

vergelijkbaar met bovenstaande antwoorden, maar dit heeft trouw gewerkt voor mij…
in een service-add:

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

Antwoord 24

Het probleem komt eigenlijk wanneer we vragen om hoekig om de verteringscyclus uit te voeren, ook al is het in proces, wat een probleem creëert dat hoekig is om te begrijpen. gevolg uitzondering in console.
1. Het heeft geen zin om scope.$apply() binnen de $timeout-functie aan te roepen, omdat het intern hetzelfde doet.
2. De code past bij de vanille JavaScript-functie omdat de native niet-hoekige hoek is gedefinieerd, d.w.z. setTimeout
3. Om dat te doen kunt u gebruik maken van

if(!scope.$$phase){
scope.$evalAsync(function(){

});
}


Antwoord 25

       let $timeoutPromise = null;
        $timeout.cancel($timeoutPromise);
        $timeoutPromise = $timeout(() => {
            $scope.$digest();
        }, 0, false);

Hier is een goede oplossing om deze fout te vermijden en $apply te vermijden

je kunt dit combineren met debounce(0) als je belt op basis van een externe gebeurtenis. Hierboven is de ‘debounce’ die we gebruiken, en een volledig voorbeeld van code

.factory('debounce', [
    '$timeout',
    function ($timeout) {
        return function (func, wait, apply) {
            // apply default is true for $timeout
            if (apply !== false) {
                apply = true;
            }
            var promise;
            return function () {
                var cntx = this,
                    args = arguments;
                $timeout.cancel(promise);
                promise = $timeout(function () {
                    return func.apply(cntx, args);
                }, wait, apply);
                return promise;
            };
        };
    }
])

en de code zelf om naar een gebeurtenis te luisteren en $digest alleen te bellen op $scope die je nodig hebt

       let $timeoutPromise = null;
        let $update = debounce(function () {
            $timeout.cancel($timeoutPromise);
            $timeoutPromise = $timeout(() => {
                $scope.$digest();
            }, 0, false);
        }, 0, false);
        let $unwatchModelChanges = $scope.$root.$on('updatePropertiesInspector', function () {
            $update();
        });
        $scope.$on('$destroy', () => {
            $timeout.cancel($update);
            $timeout.cancel($timeoutPromise);
            $unwatchModelChanges();
        });

Antwoord 26

U kunt $timeoutom de fout te voorkomen.

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

Antwoord 27

Gevonden: https://coderwall.com/p/ngismawaar Nathan Walker (bijna onderaan van pagina) stelt een decorateur in $rootScope voor om func ‘safeApply’ te maken, code:

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

Antwoord 28

Dit zal je probleem oplossen:

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

Other episodes