angularjs - use - scope watch angular example




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

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 disparando 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.

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

Tente isto:

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

A forma mais curta de $apply segura é:

$timeout(angular.noop)

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

De uma discussão recente com os caras do Angular sobre este mesmo tópico: Por razões de futuro, 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 variáveis, 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!


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 métodos 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? https://github.com/yearofmoo/AngularJS-Scope.SafeApply parece bem, mas também usa o método de inspeção da $$phase .


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

De acordo com os docs , $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 variáveis ​​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 variáveis ​​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.


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 função apply(<your scope>) 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);
  }
}

Isso resolverá seu problema:

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

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á disparar 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 estará errada: você não precisa redigir seu escopo ou não deve ser responsável por isso (veja abaixo).

Como estruturar seu código

Quando você obtém esse erro, 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 você só precisa sincronizar uma parte isolada do seu HTML (uma vez que um novo $apply será acionado se nenhum estiver em andamento), mas este é o melhor solução quando você está executando uma função 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.

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


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

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.



Você pode usar

$timeout

para evitar o erro.

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

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

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

yearofmoo fez um ótimo trabalho ao criar uma função $ 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);




angular-digest