O filtro angular funciona, mas faz com que “10 $ iterações de digitação sejam atingidas”

Eu recebo dados do meu servidor back-end estruturado assim:

{ name : "Mc Feast", owner : "Mc Donalds" }, { name : "Royale with cheese", owner : "Mc Donalds" }, { name : "Whopper", owner : "Burger King" } 

Para minha opinião, eu gostaria de “inverter” a lista. Ou seja, quero listar cada proprietário e, para esse proprietário, listar todos os hambúrgueres. Eu posso conseguir isso usando a function underscorejs groupBy em um filtro que eu uso com a diretiva ng-repeat :

JS:

 app.filter("ownerGrouping", function() { return function(collection) { return _.groupBy(collection, function(item) { return item.owner; }); } }); 

HTML:

 
  • {{owner}}:
    • {{burger.name}}
  • Isso funciona como esperado, mas recebo um enorme rastreamento de pilha de erros quando a lista é renderizada com a mensagem de erro “10 $ digest iterations reached”. Eu tenho dificuldade em ver como meu código cria um loop infinito que está implícito nessa mensagem. Alguem sabe por quê?

    Aqui está um link para um plunk com o código: http://plnkr.co/edit/8kbVuWhOMlMojp0E5Qbs?p=preview

    Isso acontece porque _.groupBy retorna uma coleção de novos objects toda vez que é executado. O ngRepeat do Angular não percebe que esses objects são iguais porque ngRepeat rastreia por identidade . Novo object leva a nova identidade. Isso faz com que o Angular pense que algo mudou desde a última verificação, o que significa que o Angular deve executar outra verificação (também conhecida como digest). O próximo resumo acaba recebendo mais um novo conjunto de objects, e assim outro resumo é acionado. As repetições até que Angular desista.

    Uma maneira fácil de se livrar do erro é garantir que o filtro retorne a mesma coleção de objects a cada vez (a menos que isso tenha mudado). Você pode fazer isso muito facilmente com sublinhado usando _.memoize . Apenas enrole a function de filtro no memoize:

     app.filter("ownerGrouping", function() { return _.memoize(function(collection, field) { return _.groupBy(collection, function(item) { return item.owner; }); }, function resolver(collection, field) { return collection.length + field; }) }); 

    Uma function de resolução é necessária se você planeja usar valores de campo diferentes para seus filtros. No exemplo acima, o comprimento da matriz é usado. É melhor reduzir a coleção para uma única string de hash md5.

    Veja garfo empilhador aqui . Memoize lembrará o resultado de uma input específica e retornará o mesmo object se a input for a mesma de antes. Se os valores forem alterados com frequência, você deverá verificar se _.memoize descarta resultados antigos para evitar um memory leaks ao longo do tempo.

    Investigando um pouco mais, vejo que o ngRepeat suporta uma syntax estendida ... track by EXPRESSION , que pode ser útil de alguma forma, permitindo que você diga ao Angular para ver o owner dos restaurantes em vez da identidade dos objects. Isso seria uma alternativa para o truque de memorização acima, embora eu não conseguisse testá-lo no plunker (possivelmente a versão antiga do Angular anterior à track by foi implementada?).

    Ok, acho que descobri. Comece dando uma olhada no código fonte do ngRepeat . Observe a linha 199: Aqui é onde nós configuramos os relógios no array / object que estamos repetindo, de forma que se ele ou seus elementos mudarem, um ciclo de digitação será triggersdo:

     $scope.$watchCollection(rhs, function ngRepeatAction(collection){ 

    Agora precisamos encontrar a definição de $watchCollection , que começa na linha 360 do rootScope.js . Esta function é passada em nossa matriz ou expressão de object, que no nosso caso é hamburgers | ownerGrouping hamburgers | ownerGrouping . Na linha 365, essa expressão de cadeia é transformada em uma function usando o serviço $parse , uma function que será invocada mais tarde, e toda vez que esse observador for executado:

     var objGetter = $parse(obj); 

    Essa nova function, que avaliará nosso filtro e obterá a matriz resultante, é chamada apenas algumas linhas abaixo:

     newValue = objGetter(self); 

    Portanto, newValue contém o resultado de nossos dados filtrados, depois que groupBy foi aplicado.

    Em seguida, vá até a linha 408 e dê uma olhada neste código:

      // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { if (oldValue[i] !== newValue[i]) { changeDetected++; oldValue[i] = newValue[i]; } } 

    Na primeira vez que executar, oldValue é apenas uma matriz vazia (configurada acima como "internalArray"), portanto, uma alteração será detectada. No entanto, cada um dos seus elementos será definido para o elemento correspondente de newValue, de forma que esperamos que na próxima vez que for executado, tudo seja compatível e nenhuma alteração seja detectada. Então, quando tudo estiver funcionando normalmente, esse código será executado duas vezes. Uma vez para a configuração, que detecta uma alteração do estado nulo inicial e, mais uma vez, porque a alteração detectada força um novo ciclo de digitação a ser executado. No caso normal, nenhuma mudança será detectada durante esta segunda execução, porque nesse ponto (oldValue[i] !== newValue[i]) será falso para todo i. É por isso que você estava vendo 2 saídas console.log no seu exemplo de trabalho.

    Mas no seu caso de falha, o seu código de filtro está gerando uma nova matriz com novas elments toda vez que ele é executado . Embora as elementações desta nova matriz tenham o mesmo valor que os elementos da matriz antiga (é uma cópia perfeita), elas não são os mesmos elementos reais . Isto é, eles se referem a objects diferentes na memory que simplesmente possuem as mesmas propriedades e valores. Portanto, no seu caso oldValue[i] !== newValue[i] sempre será verdadeiro, pela mesma razão que, por exemplo, {x: 1} !== {x: 1} é sempre verdadeiro. E uma mudança sempre será detectada.

    Portanto, o problema essencial é que seu filtro está criando uma nova cópia da matriz toda vez que é executada, consistindo em novos elementos que são cópias das elments da matriz original . Assim, a configuração watcher do ngRepeat apenas fica presa no que é essencialmente um loop recursivo infinito, sempre detectando uma mudança e acionando um novo ciclo de digestão.

    Esta é uma versão mais simples do seu código que recria o mesmo problema: http://plnkr.co/edit/KiU4v4V4i0iXmdOKesgy7t?p=preview

    O problema desaparece se o filtro parar de criar uma nova matriz toda vez que for executado.

    Uma novidade no AngularJS 1.2 é a opção “track-by” para a diretiva ng-repeat. Você pode usá-lo para ajudar o Angular a reconhecer que diferentes instâncias de objects devem ser realmente consideradas o mesmo object.

     ng-repeat="student in students track by student.id" 

    Isso ajudará a confundir o Angular em casos como o seu, onde você está usando o Underscore para fazer fatias e cortes pesados, produzindo novos objects em vez de simplesmente filtrá-los.

    Obrigado pela solução memoize, funciona bem.

    No entanto, _.memoize usa o primeiro parâmetro passado como a chave padrão para seu cache. Isso não poderia ser útil, especialmente se o primeiro parâmetro for sempre a mesma referência. Espero que este comportamento seja configurável através do parâmetro resolver .

    No exemplo abaixo, o primeiro parâmetro será sempre o mesmo array, e o segundo, uma string representando em qual campo ele deve ser agrupado por:

     return _.memoize(function(collection, field) { return _.groupBy(collection, field); }, function resolver(collection, field) { return collection.length + field; }); 

    Perdoe a brevidade, mas tente ng-init="thing = (array | fn:arg)" e use uma thing na sua ng-repeat . Funciona para mim, mas esta é uma questão ampla.

    Não sei por que esse erro está chegando, mas, logicamente, a function de filtro é chamada para cada elemento da matriz.

    No seu caso, a function de filtro que você criou retorna uma function que só deve ser chamada quando a matriz é atualizada, não para cada elemento da matriz. O resultado retornado pela function pode ser limitado a html.

    Eu bifurquei o plunker e criei minha própria implementação dele aqui http://plnkr.co/edit/KTlTfFyVUhWVCtX6igsn

    Não usa nenhum filtro. A idéia básica é chamar o groupBy no início e sempre que um elemento for adicionado

     $scope.ownerHamburgers=_.groupBy(hamburgers, function(item) { return item.owner; }); $scope.addBurger = function() { hamburgers.push({ name : "Mc Fish", owner :"Mc Donalds" }); $scope.ownerHamburgers=_.groupBy(hamburgers, function(item) { return item.owner; }); } 

    Por que vale a pena, para adicionar mais um exemplo e solução, eu tinha um filtro simples como este:

     .filter('paragraphs', function () { return function (text) { return text.split(/\n\n/g); } }) 

    com:

     

    {{ p }}

    que causou a recursion infinita descrita em $digest . Foi facilmente corrigido com:

     

    {{ p }}

    Isso também é necessário, pois o ngRepeat paradoxalmente não gosta de repetidores, isto é, "foo\n\nfoo" causaria um erro por causa de dois parágrafos idênticos. Esta solução pode não ser apropriada se o conteúdo dos parágrafos estiver realmente mudando e é importante que eles continuem sendo digeridos, mas no meu caso isso não é um problema.