Por que usar if (! $ Scope. $$ phase) $ scope. $ Apply () é um antipadrão?

92

Às vezes, preciso usar $scope.$applyno meu código e às vezes gera um erro de "resumo já em andamento". Então, comecei a encontrar uma maneira de contornar isso e encontrei esta pergunta: AngularJS: Impedir o erro $ digest já em andamento ao chamar $ scope. $ Apply () . No entanto, nos comentários (e no wiki do angular) você pode ler:

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

Então agora eu tenho duas perguntas:

  1. Por que exatamente isso é um antipadrão?
  2. Como posso usar $ scope. $ Apply com segurança?

Outra "solução" para evitar o erro "resumo já em andamento" parece estar usando $ timeout:

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

Aquele é o caminho para ir? É mais seguro? Portanto, aqui está a verdadeira questão: Como posso eliminar totalmente a possibilidade de um erro de "resumo já em andamento"?

PS: Estou usando apenas $ scope. $ Apply em callbacks não angularjs que não são síncronos. (pelo que eu sei, essas são situações em que você deve usar $ scope. $ apply se quiser que suas alterações sejam aplicadas)

Dominik Goltermann
fonte
Pela minha experiência, você deve sempre saber se está manipulando scopede dentro do angular ou de fora do angular. Então de acordo com isso você sempre sabe se precisa ligar scope.$applyou não. E se você estiver usando o mesmo código para scopemanipulação angular / não angular , você está fazendo errado, ele deve estar sempre separado ... então, basicamente, se você se deparar com um caso em que precisa verificar scope.$$phase, seu código não é projetado de forma correta, e sempre há uma maneira de fazê-lo 'da maneira certa'
doodeec
1
estou usando apenas em chamadas de retorno não angulares (!) É por isso que estou confuso
Dominik Goltermann
2
se não fosse angular, não geraria digest already in progresserro
doodeec
1
Isso foi o que eu pensei. A questão é: nem sempre lança o erro. Só de vez em quando. Minha suspeita é que o aplicativo colide POR ACaso com outro resumo. Isso é possível?
Dominik Goltermann
Não acho que isso seja possível se o retorno de chamada for estritamente não angular
doodeec

Respostas:

113

Depois de mais algumas pesquisas, consegui resolver a questão de saber se é sempre seguro usar $scope.$apply. A resposta curta é sim.

Resposta longa:

Devido à forma como seu navegador executa Javascript, não é possível que duas chamadas de resumo entrem em conflito por acaso .

O código JavaScript que escrevemos não é executado de uma vez, mas em turnos. Cada uma dessas curvas é executada ininterruptamente do início ao fim e, quando uma curva está acontecendo, nada mais acontece em nosso navegador. (de http://jimhoskins.com/2012/12/17/angularjs-and-apply.html )

Portanto, o erro "resumo já em andamento" só pode ocorrer em uma situação: Quando um $ apply é emitido dentro de outro $ apply, por exemplo:

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

Esta situação não pode ocorrer se usarmos $ scope.apply em um callback não angularjs puro, como por exemplo o callback de setTimeout. Portanto, o código a seguir é 100% à prova de balas e não necessidade de fazer umif (!$scope.$$phase) $scope.$apply()

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

mesmo este é seguro:

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

O que NÃO é seguro (porque $ timeout - como todos os ajudantes angularjs - já chama $scope.$applypor você):

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

Isso também explica por que o uso de if (!$scope.$$phase) $scope.$apply()é um antipadrão. Você simplesmente não precisa disso se usar $scope.$applyda maneira correta: Em um callback puro js como setTimeoutpor exemplo.

Leia http://jimhoskins.com/2012/12/17/angularjs-and-apply.html para obter uma explicação mais detalhada.

Dominik Goltermann
fonte
Eu tenho um exemplo onde eu crio um serviço com $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });Eu realmente não sei por que tenho que fazer $ aplicar aqui, porque estou usando $ document.bind ..
Betty St
porque $ document é apenas "Um wrapper jQuery ou jqLite para o objeto window.document do navegador." e implementado da seguinte forma: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }Não há nenhum aplicativo ali.
Dominik Goltermann
11
$timeoutsemanticamente significa executar o código após um atraso. Pode ser uma coisa funcionalmente segura de se fazer, mas é um hack. Deve haver uma maneira segura de usar $ apply quando você não consegue saber se um $digestciclo está em andamento ou se já está dentro de um $apply.
John Strickler
1
outro motivo pelo qual é ruim: ele usa variáveis ​​internas ($$ phase) que não fazem parte da API pública e podem ser alteradas em uma versão mais recente do angular e, assim, quebrar seu código. Seu problema com o acionamento de eventos síncronos é interessante
Dominik Goltermann
4
A abordagem mais recente é usar $ scope. $ EvalAsync (), que executa com segurança no ciclo de resumo atual, se possível, ou no próximo ciclo. Consulte bennadel.com/blog/…
jaymjarri
16

É definitivamente um antipadrão agora. Eu vi um resumo explodir mesmo se você verificar a fase $$. Você simplesmente não deve acessar a API interna indicada por $$prefixos.

Você deveria usar

 $scope.$evalAsync();

como este é o método preferido em Angular ^ 1.4 e é especificamente exposto como uma API para a camada de aplicativo.

FlavorScape
fonte
9

Em qualquer caso, quando seu resumo está em andamento e você envia outro serviço para fazer o resumo, isso simplesmente dá um erro, ou seja, um resumo já está em andamento. então, para curar isso, você tem duas opções. você pode verificar se há outro resumo em andamento, como uma votação.

Primeiro

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

se a condição acima for verdadeira, você pode aplicar seu $ escopo. $ aplicar de outra forma não e

a segunda solução é usar $ timeout

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

ele não permitirá que o outro resumo comece até $ timeout completar sua execução.

Lalit Sachdeva
fonte
1
downvoted; A questão pergunta especificamente por que NÃO fazer o que você está descrevendo aqui, não há outra maneira de contornar isso. Veja a excelente resposta de @gaul para saber quando usar $scope.$apply();.
PureSpider
Apesar de não responder à pergunta: $timeouté a chave! funciona e depois descobri que também é recomendado.
Himel Nag Rana
Eu sei que é muito tarde para adicionar comentários a isso 2 anos depois, mas
tome
9

scope.$applydesencadeia um $digestciclo que é fundamental para a vinculação de dados bidirecional

Um $digestciclo verifica se há objetos, ou seja, modelos (para ser mais preciso $watch) anexados para $scopeavaliar se seus valores mudaram e se ele detecta uma mudança, ele executa as etapas necessárias para atualizar a visualização.

Agora, quando você usa, $scope.$applyencontra um erro "Já em andamento", então é bastante óbvio que um $ digest está sendo executado, mas o que o acionou?

ans -> todas as $httpchamadas, todos os ng-click, repetir, mostrar, ocultar etc acionam um $digestciclo E A PIOR PARTE FUNCIONA EM CADA $ ESCOPO.

ou seja, digamos que sua página tenha 4 controladores ou diretivas A, B, C, D

Se você tiver 4 $scopepropriedades em cada um deles, terá um total de 16 $ propriedades de escopo em sua página.

Se você disparar $scope.$applyno controlador D, um $digestciclo verificará todos os 16 valores !!! mais todas as propriedades $ rootScope.

Resposta -> mas $scope.$digestaciona um $digestfilho e o mesmo escopo, portanto, verificará apenas 4 propriedades. Portanto, se você tiver certeza de que as mudanças em D não afetarão A, B, C, use $scope.$digest não $scope.$apply.

Portanto, um mero ng-click ou ng-show / hide pode disparar um $digestciclo em mais de 100 propriedades, mesmo quando o usuário não disparou nenhum evento !

Rishul Matta
fonte
2
Sim, eu percebi isso tarde no projeto, infelizmente. Não teria usado o Angular se soubesse disso desde o início. Todas as diretivas padrão disparam um $ scope. $ Apply, que por sua vez chama $ rootScope. $ Digest, que executa verificações sujas em TODOS os escopos. Má decisão de design, se você me perguntar. Devo estar no controle de quais escopos devem ser verificados de maneira incorreta, porque SEI COMO OS DADOS ESTÃO VINCULADOS A ESTES ESCOPOS!
MoonStom
0

Use $timeout, é a forma recomendada.

Meu cenário é que preciso alterar itens na página com base nos dados que recebi de um WebSocket. E como está fora do Angular, sem o $ timeout, o único modelo será alterado, mas não a visualização. Porque o Angular não sabe que esse dado foi alterado. $timeoutbasicamente diz ao Angular para fazer a alteração na próxima rodada de $ digest.

Tentei o seguinte também e funcionou. A diferença para mim é que $ timeout é mais claro.

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)
James J. Ye
fonte
É muito mais limpo envolver seu código de soquete em $ apply (muito parecido com o do Angular no código AJAX, ou seja $http). Caso contrário, você terá que repetir este código em todo o lugar.
timruffles
isso definitivamente não é recomendado. Além disso, você ocasionalmente obterá um erro ao fazer isso se $ scope tiver $$ phase. em vez disso, você deve usar $ scope. $ evalAsync ();
FlavorScape
Não há necessidade de $scope.$applyse você estiver usando setTimeoutou$timeout
Kunal
-1

Achei uma solução muito legal:

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

injete onde você precisa:

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