Perché usare if (! $ Scope. $$ phase) $ scope. $ Apply () un anti-pattern?


92

A volte ho bisogno di usarlo $scope.$applynel mio codice ea volte genera un errore di "digest già in corso". Quindi ho iniziato a trovare un modo per aggirare questo problema e ho trovato questa domanda: AngularJS: Prevenire l'errore $ digest già in corso quando si chiama $ scope. $ Apply () . Tuttavia nei commenti (e sul wiki angolare) puoi leggere:

Non farlo se (! $ Scope. $$ fase) $ scope. $ Apply (), significa che il tuo $ scope. $ Apply () non è abbastanza alto nello stack di chiamate.

Quindi ora ho due domande:

  1. Perché esattamente questo è un anti-pattern?
  2. Come posso utilizzare in sicurezza $ scope. $ Apply?

Un'altra "soluzione" per prevenire l'errore "digest già in corso" sembra utilizzare $ timeout:

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

È questa la strada da percorrere? È più sicuro? Quindi ecco la vera domanda: come posso eliminare completamente la possibilità di un errore di "digest già in corso"?

PS: Sto usando solo $ scope. $ Apply nei callback non angularjs che non sono sincroni. (per quanto ne so, queste sono situazioni in cui è necessario utilizzare $ scope. $ apply se si desidera applicare le modifiche)


Dalla mia esperienza, dovresti sempre sapere se stai manipolando scopedall'interno angolare o dall'esterno angolare. Quindi in base a questo sai sempre se devi chiamare scope.$applyo meno. E se stai usando lo stesso codice sia per la scopemanipolazione angolare / non angolare , stai sbagliando, dovrebbe essere sempre separato ... quindi in pratica se ti imbatti in un caso in cui devi controllare scope.$$phase, il tuo codice non lo è progettato in modo corretto, e c'è sempre un modo per farlo 'nel modo giusto'
doodeec

1
Lo sto usando solo in callback non angolari (!) Questo è il motivo per cui sono confuso
Dominik Goltermann

2
se non fosse angolare, non genererebbe digest already in progresserrori
doodeec

1
è quello che pensavo. Il fatto è: non genera sempre l'errore. Solo una volta ogni tanto. Il mio sospetto è che l'applicazione si scontri PER CASO con un altro digest. È possibile?
Dominik Goltermann

Non penso che sia possibile se la richiamata è rigorosamente non angolare
doodeec

Risposte:


113

Dopo un po 'di ricerca sono riuscito a risolvere la domanda se è sempre sicuro da usare $scope.$apply. La risposta breve è sì.

Risposta lunga:

A causa del modo in cui il tuo browser esegue Javascript, non è possibile che due chiamate digest si scontrino per caso .

Il codice JavaScript che scriviamo non viene eseguito tutto in una volta, ma viene eseguito a turno. Ciascuno di questi turni si svolge ininterrottamente dall'inizio alla fine e quando un turno è in corso, non accade nient'altro nel nostro browser. (da http://jimhoskins.com/2012/12/17/angularjs-and-apply.html )

Quindi l'errore "digest già in corso" può verificarsi solo in una situazione: Quando un $ apply viene emesso all'interno di un altro $ apply, ad esempio:

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

Questa situazione non può verificarsi se usiamo $ scope.apply in un callback puro non angularjs, come ad esempio il callback di setTimeout. Così il seguente codice è 100% a prova di proiettile e non v'è alcun bisogno di fare unaif (!$scope.$$phase) $scope.$apply()

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

anche questo è sicuro:

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

Cosa NON è sicuro (perché $ timeout - come tutti gli helper di angularjs - $scope.$applyti chiama già ):

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

Questo spiega anche perché l'uso di if (!$scope.$$phase) $scope.$apply()è un anti-pattern. Semplicemente non ne hai bisogno se lo usi $scope.$applynel modo corretto: in un callback js puro come setTimeoutper esempio.

Leggi http://jimhoskins.com/2012/12/17/angularjs-and-apply.html per una spiegazione più dettagliata.


Ho un esempio in cui creo un servizio con $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });Non so davvero perché devo fare $ apply qui, perché sto usando $ document.bind ..
Betty St

perché $ document è solo "Un wrapper jQuery o jqLite per l'oggetto window.document del browser." e implementato come segue: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }non è applicabile.
Dominik Goltermann

11
$timeoutsemanticamente significa eseguire codice dopo un ritardo. Potrebbe essere una cosa funzionalmente sicura da fare, ma è un trucco. Dovrebbe esserci un modo sicuro per utilizzare $ apply quando non sei in grado di sapere se un $digestciclo è in corso o sei già all'interno di un file $apply.
John Strickler

1
un altro motivo per cui è cattivo: utilizza variabili interne (fase $$) che non fanno parte dell'API pubblica e potrebbero essere modificate in una versione più recente di angular e quindi rompere il codice. Il tuo problema con l'attivazione di eventi sincroni è interessante però
Dominik Goltermann

4
L'approccio più recente consiste nell'usare $ scope. $ EvalAsync () che viene eseguito in modo sicuro nel ciclo digest corrente, se possibile, o nel ciclo successivo. Fare riferimento a bennadel.com/blog/…
jaymjarri

16

Adesso è decisamente un anti-pattern. Ho visto un riassunto esplodere anche se controlli la fase $$. Semplicemente non dovresti accedere all'API interna indicata da $$prefissi.

Dovresti usare

 $scope.$evalAsync();

poiché questo è il metodo preferito in Angular ^ 1.4 ed è specificamente esposto come API per il livello dell'applicazione.


9

In ogni caso, quando il tuo digest è in corso e spingi un altro servizio a digerire, dà semplicemente un errore cioè digest già in corso. quindi per curare questo hai due opzioni. puoi controllare qualsiasi altro digest in corso come i sondaggi.

Il primo

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

se la condizione precedente è vera, allora puoi applicare il tuo $ scope. $ apply otherwies not e

la seconda soluzione è usare $ timeout

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

non lascerà avviare l'altro digest fino a quando $ timeout non avrà completato la sua esecuzione.


1
downvoted; La domanda chiede specificamente perché NON fare la cosa che stai descrivendo qui, non per un altro modo per aggirarla. Vedi l'eccellente risposta di @gaul per quando usare $scope.$apply();.
PureSpider

Pur non rispondendo alla domanda: $timeoutè la chiave! funziona e in seguito ho scoperto che è consigliato anche.
Himel Nag Rana

So che è abbastanza tardi per aggiungere un commento a questo 2 anni dopo, ma fai attenzione quando usi troppo $ timeout, poiché questo può
costarti

9

scope.$applyinnesca un $digestciclo fondamentale per il data binding a 2 vie

Un $digestciclo verifica la presenza di oggetti, ad esempio modelli (per la precisione $watch) allegati per $scopevalutare se i loro valori sono cambiati e se rileva una modifica, sono necessari i passaggi necessari per aggiornare la vista.

Ora, quando si utilizza, $scope.$applysi verifica un errore "Già in corso" quindi è abbastanza ovvio che sia in esecuzione un $ digest ma cosa lo ha attivato?

ans -> ogni $httpchiamata, tutti i clic, ripetere, mostrare, nascondere ecc. attivano un $digestciclo E LA PARTE PEGGIORE CHE CORRE IN OGNI $ SCOPE.

ad esempio, supponiamo che la tua pagina abbia 4 controller o direttive A, B, C, D

Se hai 4 $scopeproprietà in ciascuna di esse, hai un totale di 16 proprietà $ scope sulla tua pagina.

Se esegui il trigger $scope.$applynel controller D, un $digestciclo controllerà tutti i 16 valori !!! più tutte le proprietà $ rootScope.

Risposta -> ma $scope.$digestattiva un $digestfiglio su e lo stesso ambito, quindi controllerà solo 4 proprietà. Quindi, se sei sicuro che i cambiamenti in D non influenzeranno A, B, C allora usa $scope.$digest no $scope.$apply.

Quindi un semplice clic o mostra / nascondi potrebbe innescare un $digestciclo su oltre 100 proprietà anche quando l'utente non ha attivato alcun evento !


2
Sì, l'ho capito in ritardo nel progetto, purtroppo. Non avrei usato Angular se lo avessi saputo dall'inizio. Tutte le direttive standard attivano un $ scope. $ Apply, che a sua volta chiama $ rootScope. $ Digest, che esegue controlli sporchi su TUTTI gli ambiti. Scarsa decisione di progettazione se me lo chiedi. Dovrei avere il controllo di quali ambiti devono essere controllati, perché SO COME I DATI SONO COLLEGATI A QUESTI SCOPI!
MoonStom

0

Usa $timeout, è il modo consigliato.

Il mio scenario è che devo modificare gli elementi sulla pagina in base ai dati che ho ricevuto da un WebSocket. E poiché è al di fuori di Angular, senza $ timeout, verrà modificato l'unico modello ma non la vista. Perché Angular non sa che quel dato è stato cambiato. $timeoutsta fondamentalmente dicendo ad Angular di apportare la modifica nel prossimo round di $ digest.

Ho provato anche quanto segue e funziona. La differenza per me è che $ timeout è più chiaro.

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

È molto più pulito avvolgere il codice del tuo socket in $ apply (proprio come Angular sul codice AJAX, cioè $http). Altrimenti devi ripetere questo codice dappertutto.
timruffles

questo è decisamente sconsigliato. Inoltre, occasionalmente riceverai un errore se $ scope ha la fase $$. invece, dovresti usare $ scope. $ evalAsync ();
FlavorScape

Non è necessario $scope.$applyse si utilizza setTimeouto$timeout
Kunal

-1

Ho trovato una soluzione molto interessante:

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

iniettalo dove ti serve:

.controller('MyCtrl', ['$scope', 'safeApply',
    function($scope, safeApply) {
        safeApply($scope); // no function passed in
        safeApply($scope, function() { // passing a function in
        });
    }
])
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.