AngularJS: Previne o erro $ digest já em andamento ao chamar o $ scope. $ Apply ()

Eu estou achando que preciso atualizar minha página para o meu escopo manualmente mais e mais desde a construção de um aplicativo em angular.

A única maneira que conheço para fazer isso é chamar $apply() do escopo dos meus controllers e diretivas. O problema com isso é que ele continua lançando um erro no console que lê:

Erro: $ digest já em andamento

Alguém sabe como evitar esse erro ou conseguir a mesma coisa, mas de uma maneira diferente?

Não use esse padrão – isso vai acabar causando mais erros do que resolve. Mesmo que você pense que consertou alguma coisa, isso não aconteceu.

Você pode verificar se um $digest já está em andamento, verificando a $scope.$$phase .

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

$scope.$$phase retornará "$digest" ou "$apply" se um $digest ou $apply estiver em andamento. Acredito que a diferença entre esses estados é que $digest processará os relógios do escopo atual e seus filhos, e $apply processará os observadores de todos os escopos.

Para o ponto de @ dnc253, se você estiver chamando $digest ou $apply frequência, você pode estar fazendo errado. Eu geralmente acho que preciso digerir quando eu preciso atualizar o estado do escopo como resultado de um evento DOM triggersndo fora do alcance do Angular. Por exemplo, quando um modal de bootstrap do twitter fica oculto. Às vezes, o evento DOM é acionado quando um $digest está em andamento, às vezes não. É por isso que uso esse cheque.

Eu adoraria saber uma maneira melhor se alguém conhece um.


Comentários: por @anddoutoi

angular.js Anti Padrões

  1. Não faça if (!$scope.$$phase) $scope.$apply() , isso significa que seu $scope.$apply() não é alto o suficiente na pilha de chamadas.

De uma discussão recente com os caras do Angular sobre este mesmo tópico: Por razões futuras, você não deve usar a $$phase

Quando pressionado pela maneira “certa” de fazê-lo, a resposta é atualmente

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

Recentemente encontrei isso quando escrevi serviços angulares para agrupar as APIs do Facebook, do Google e do Twitter que, em diferentes graus, recebem retornos de chamada.

Aqui está um exemplo de dentro de um serviço. (Por uma questão de brevidade, o resto do serviço – que configurou as variables, injetou $ timeout etc. – foi deixado de fora.)

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

Observe que o argumento de atraso para $ timeout é opcional e será padronizado como 0 se não for definido ( $ timeout chama $ browser.defer, cujo valor padrão é 0 se o atraso não for definido )

Um pouco não-intuitivo, mas essa é a resposta dos caras escrevendo Angular, então é bom o suficiente para mim!

O ciclo de digitação é uma chamada síncrona. Ele não renderá controle ao loop de events do navegador até que seja feito. Existem algumas maneiras de lidar com isso. A maneira mais fácil de lidar com isso é usar o built-in $ timeout, e uma segunda maneira é se você estiver usando sublinhado ou lodash (e deveria estar), chame o seguinte:

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

ou se você tiver sublinhado:

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

Tentamos várias soluções alternativas e odiamos injetar o $ rootScope em todos os nossos controladores, diretivas e até mesmo algumas fábricas. Então, o $ timeout e o _.defer foram nossos favoritos até agora. Esses methods informam com êxito o ângulo para aguardar até o próximo loop de animação, o que garantirá que o escopo atual. $ Apply termine.

Muitas das respostas aqui contêm bons conselhos, mas também podem causar confusão. Simplesmente usar $timeout não é a melhor nem a solução correta. Além disso, certifique-se de ler isso se estiver preocupado com performances ou escalabilidade.

Coisas que você deveria saber

  • $$phase é privada para o framework e há boas razões para isso.

  • $timeout(callback) esperará até que o ciclo de digitação atual (se houver) seja concluído, então execute o retorno de chamada e, em seguida, execute no final um $apply total $apply .

  • $timeout(callback, delay, false) fará o mesmo (com um atraso opcional antes de executar o callback), mas não irá triggersr um $apply (terceiro argumento) que salva performances se você não modificou seu modelo Angular ($ scope ).

  • $scope.$apply(callback) invoca, entre outras coisas, $rootScope.$digest , o que significa que ele redigirá o escopo raiz do aplicativo e todos os seus filhos, mesmo se você estiver dentro de um escopo isolado.

  • $scope.$digest() irá simplesmente sincronizar seu modelo com a visão, mas não irá digerir o escopo de seus pais, o que pode economizar muitos desempenhos ao trabalhar em uma parte isolada do seu HTML com um escopo isolado (a partir de uma diretiva) . $ digest não recebe um retorno de chamada: você executa o código e digita.

  • $scope.$evalAsync(callback) foi introduzido com o angularjs 1.2, e provavelmente resolverá a maioria dos seus problemas. Por favor, consulte o último parágrafo para saber mais sobre isso.

  • Se você receber o $digest already in progress error , sua arquitetura está errada: você não precisa redigir seu escopo, ou você não deve ser responsável por isso (veja abaixo).

Como estruturar seu código

Quando você obtém esse erro, você está tentando digerir seu escopo enquanto ele já está em andamento: como você não sabe o estado do seu escopo nesse ponto, você não está encarregado de lidar com a digestão.

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

E se você sabe o que está fazendo e trabalha em uma pequena diretiva isolada enquanto faz parte de um grande aplicativo Angular, você pode preferir $ digest em vez de $ apply para salvar performances.

Atualização desde Angularjs 1.2

Um novo e poderoso método foi adicionado a qualquer $ escopo: $evalAsync . Basicamente, ele executará seu retorno de chamada dentro do ciclo de digitação atual se um estiver ocorrendo, caso contrário, um novo ciclo de digitação iniciará a execução do retorno de chamada.

Isso ainda não é tão bom quanto um $scope.$digest se você realmente sabe que precisa sincronizar apenas uma parte isolada do seu HTML (uma vez que um novo $apply será acionado se nenhum estiver em progresso), mas este é o melhor solução quando você está executando uma function que você não pode saber se será executada de forma síncrona ou não , por exemplo após buscar um recurso potencialmente armazenado em cache: às vezes isso exigirá uma chamada assíncrona para um servidor, caso contrário o recurso será buscado localmente de forma síncrona.

Nestes casos e em todos os outros em que você teve uma !$scope.$$phase $scope.$evalAsync( callback ) !$scope.$$phase , use $scope.$evalAsync( callback )

Útil pequeno método auxiliar para manter este processo DRY:

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

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

O problema surge quando você tem uma chamada para $apply que às vezes é executada de forma assíncrona fora do código Angular (quando $ apply deve ser usado) e às vezes sincronamente dentro do código Angular (que causa o erro $digest already in progress ).

Isso pode acontecer, por exemplo, quando você tem uma biblioteca que obtém itens de forma assíncrona de um servidor e os armazena em cache. Na primeira vez que um item é solicitado, ele será recuperado de forma assíncrona para não bloquear a execução do código. Na segunda vez, no entanto, o item já está no cache para que possa ser recuperado de forma síncrona.

A maneira de evitar esse erro é garantir que o código que chama $apply seja executado de forma assíncrona. Isso pode ser feito executando seu código dentro de uma chamada para $timeout com o atraso definido como 0 (que é o padrão). No entanto, chamar seu código dentro de $timeout elimina a necessidade de chamar $apply , porque $ timeout acionará outro ciclo $digest por conta própria, que, por sua vez, fará todas as atualizações necessárias, etc.

Solução

Em suma, em vez de fazer isso:

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

faça isso:

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

Apenas $apply call se $apply quando você sabe que o código em execução sempre será executado fora do código Angular (por exemplo, sua chamada para $ apply acontecerá dentro de um retorno de chamada que é chamado pelo código fora do seu código angular).

A menos que alguém esteja ciente de alguma desvantagem de usar $timeout over $apply , não vejo porque você não pode usar $timeout (com zero delay) ao invés de $apply , já que ele fará aproximadamente a mesma coisa.

Eu tive o mesmo problema com scripts de terceiros, como CodeMirror, por exemplo, e Krpano, e mesmo usando methods safeApply mencionados aqui não resolvi o erro para mim.

Mas o que resolveu foi usar o serviço $ timeout (não esqueça de injetar primeiro).

Assim, algo como:

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

e se dentro do seu código você estiver usando

esta

talvez porque esteja dentro de um controlador de uma diretiva de fábrica ou apenas precise de algum tipo de encadernação, então você faria algo como:

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

Quando você recebe este erro, isso basicamente significa que ele já está atualizando sua visão. Você realmente não deveria precisar chamar $apply() dentro de seu controlador. Se a sua visualização não está atualizando como seria de esperar, e você recebe este erro depois de chamar $apply() , isso provavelmente significa que você não está atualizando o modelo corretamente. Se você postar alguns detalhes, poderíamos descobrir o problema central.

A forma mais curta de $apply segura é:

 $timeout(angular.noop) 

Você também pode usar o evalAsync. Ele será executado algum tempo depois que o resumo terminar!

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

Às vezes, você ainda receberá erros se usar dessa maneira ( https://stackoverflow.com/a/12859093/801426 ).

Tente isto:

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

Primeiro de tudo, não corrija dessa maneira

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

Não faz sentido porque $ phase é apenas uma bandeira booleana para o ciclo $ digest, então seu $ apply () às vezes não roda. E lembre-se que é uma má prática.

Em vez disso, use $timeout

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

Se você estiver usando sublinhado ou lodash, você pode usar defer ():

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

Você deve usar $ evalAsync ou $ timeout de acordo com o contexto.

Este é um link com uma boa explicação:

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

Aconselho você a usar um evento personalizado em vez de acionar um ciclo de digitação.

Descobri que transmitir events personalizados e registrar ouvintes para esses events é uma boa solução para acionar uma ação que você deseja que ocorra ou não em um ciclo de digitação.

Ao criar um evento personalizado, você também está sendo mais eficiente com seu código, pois você está apenas acionando listeners inscritos no dito evento e NÃO acionando todos os relógios vinculados ao escopo como você faria se invocasse o escopo. $ Apply.

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

yearofmoo fez um ótimo trabalho ao criar uma function $ safeApply reutilizável para nós:

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

Uso:

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

Eu consegui resolver este problema chamando $eval invés de $apply em lugares onde eu sei que a function $digest estará rodando.

De acordo com os documentos , $apply basicamente faz isso:

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

No meu caso, um ng-click altera uma variável dentro de um escopo, e um $ watch nessa variável altera outras variables ​​que devem ser $applied . Este último passo faz com que o erro “digerir já em progresso”.

Substituindo $apply por $eval dentro da expressão watch, as variables ​​de escopo são atualizadas conforme o esperado.

Portanto, parece que se o digest estiver sendo executado de qualquer maneira por causa de alguma outra mudança dentro do Angular, $eval ‘ing é tudo que você precisa fazer.

use $scope.$$phase || $scope.$apply(); $scope.$$phase || $scope.$apply(); em vez de

tente usar

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

ao invés de

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

$ applyAsync Programe a invocação de $ apply para ocorrer mais tarde. Isso pode ser usado para enfileirar várias expressões que precisam ser avaliadas no mesmo resumo.

NOTA: Dentro do $ digest, $ applyAsync () será liberado apenas se o escopo atual for o $ rootScope. Isso significa que se você chamar $ digest em um escopo filho, ele não irá liberar implicitamente a fila $ applyAsync ().

Exemplo:

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

Referências:

1. Escopo. $ ApplyAsync () vs. Escopo. $ EvalAsync () no AngularJS 1.3

  1. AngularJs Docs

Entendendo que os documentos angulares chamam checando a $$phase um _.defer , eu tentei fazer com que $timeout e _.defer funcionassem.

O tempo limite e os methods adiados criam um flash de conteúdo {{myVar}} não analisado no dom como um FOUT . Para mim isso não era aceitável. Deixa-me sem muito a ser dito dogmaticamente que algo é um hack, e não tem uma alternativa adequada.

A única coisa que funciona sempre é:

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

Eu não entendo o perigo deste método, ou porque ele é descrito como um hack pelas pessoas nos comentários e na equipe angular. O comando parece preciso e fácil de ler:

“Faça o resumo a menos que um já esteja acontecendo”

Em CoffeeScript é ainda mais bonito:

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

Qual é o problema com isso? Existe uma alternativa que não crie um FOUT? O $ safeApply parece bem, mas também usa o método de inspeção da $$phase .

Este é o meu serviço de utilitários:

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

e este é um exemplo para seu uso:

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

Eu tenho usado esse método e parece funcionar perfeitamente bem. Isso apenas aguarda a hora em que o ciclo terminou e, em seguida, os gatilhos se apply() . Simplesmente chame a function apply() de qualquer lugar que você quiser.

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

semelhante às respostas acima, mas isso tem funcionado fielmente para mim … em um serviço adicionar:

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

Você pode usar

$timeout

para evitar o erro.

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

Achei isso: https://coderwall.com/p/ngisma, onde Nathan Walker (próximo ao fim da página) sugere um decorador em $ rootScope para criar func ‘safeApply’, código:

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

Isso resolverá seu problema:

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