El filtro angular funciona pero causa que se scopen “10 $ iteraciones de digestión”

Recibo datos de mi servidor back-end estructurado de esta manera:

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

Para mi vista, me gustaría “invertir” la lista. Es decir, quiero enumerar a cada propietario y, para ese propietario, enumerar todas las hamburguesas. Puedo lograr esto usando el grupo de funciones groupBy en un filtro que luego uso con la directiva ng-repeat :

JS:

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

HTML:

 
  • {{owner}}:
    • {{burger.name}}
  • Esto funciona como se esperaba, pero obtengo un enorme rastro de la stack de errores cuando la lista se representa con el mensaje de error “se han alcanzado las iteraciones de 10 $ digest”. Me es difícil ver cómo mi código crea un ciclo infinito que está implícito en este mensaje. ¿Alguien sabe por qué?

    Aquí hay un enlace a un plunk con el código: http://plnkr.co/edit/8kbVuWhOMlMojp0E5Qbs?p=preview

    Esto sucede porque _.groupBy devuelve una colección de objetos nuevos cada vez que se ejecuta. Angular’s ngRepeat no se da cuenta de que esos objetos son iguales porque ngRepeat rastrea por identidad . Un nuevo objeto conduce a una nueva identidad. Esto hace que Angular piense que algo ha cambiado desde la última comprobación, lo que significa que Angular debería ejecutar otra comprobación (también conocida como resumen). El siguiente resumen termina obteniendo un nuevo conjunto de objetos, por lo que se desencadena otro resumen. Las repeticiones hasta que Angular se da por vencido.

    Una manera fácil de deshacerse del error es asegurarse de que su filtro devuelva la misma colección de objetos cada vez (a menos que, por supuesto, haya cambiado). Puedes hacer esto muy fácilmente con guiones bajos usando _.memoize . Simplemente ajuste la función de filtro en 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; }) }); 

    Se requiere una función de resolución si planea usar diferentes valores de campo para sus filtros. En el ejemplo anterior, se usa la longitud de la matriz. Es mejor reducir la colección a una cadena de hash md5 única.

    Vea el tenedor plunker aquí . Memoize recordará el resultado de una entrada específica y devolverá el mismo objeto si la entrada es la misma que antes. Si los valores cambian con frecuencia, entonces debe verificar si _.memoize descarta los resultados anteriores para evitar una pérdida de memoria con el tiempo.

    Investigando un poco más veo que ngRepeat soporta una syntax extendida ... track by EXPRESSION , lo que podría ser útil de alguna manera al permitirle a Angular mirar al owner de los restaurantes en lugar de a la identidad de los objetos. Esta sería una alternativa al truco de memorización anterior, aunque no pude lograr probarlo en el plunker (¿se implementó la versión anterior de Angular antes de la track by ?).

    De acuerdo, creo que lo descubrí. Comience por echar un vistazo al código fuente de ngRepeat . Observe la línea 199: aquí es donde configuramos los relojes en el conjunto / objeto sobre el que estamos repitiendo, de modo que si él o sus elementos cambian, se activará un ciclo de resumen:

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

    Ahora necesitamos encontrar la definición de $watchCollection , que comienza en la línea 360 de rootScope.js . Esta función se pasa en nuestra matriz o expresión de objeto, que en nuestro caso es hamburgers | ownerGrouping hamburgers | ownerGrouping . En la línea 365, esa expresión de cadena se convierte en una función usando el servicio $parse , una función que se invocará más tarde, y cada vez que este vigilante se ejecuta:

     var objGetter = $parse(obj); 

    Esa nueva función, que evaluará nuestro filtro y obtendrá la matriz resultante, se invoca solo unas líneas abajo:

     newValue = objGetter(self); 

    Entonces newValue contiene el resultado de nuestros datos filtrados, después de que se haya aplicado groupBy.

    Luego desplácese hacia abajo hasta la línea 408 y eche un vistazo a este 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]; } } 

    La primera vez que se ejecuta, oldValue es solo una matriz vacía (configurada anteriormente como "internalArray"), por lo que se detectará un cambio. Sin embargo, cada uno de sus elementos se establecerá en el elemento correspondiente de newValue, por lo que esperamos que la próxima vez que se ejecute, todo coincida y no se detecten cambios. Entonces, cuando todo funciona normalmente, este código se ejecutará dos veces. Una vez para la configuración, que detecta un cambio desde el estado nulo inicial, y luego una vez más, porque el cambio detectado obliga a ejecutar un nuevo ciclo de resumen. En el caso normal, no se detectarán cambios durante esta segunda ejecución, porque en ese punto (oldValue[i] !== newValue[i]) será falso para todo i. Es por eso que estaba viendo 2 salidas console.log en su ejemplo de trabajo.

    Pero en su caso fallido, su código de filtro está generando una nueva matriz con nuevos elementos cada vez que se ejecuta . Si bien los elementos de esta nueva matriz tienen el mismo valor que los elementos de la matriz anterior (es una copia perfecta), no son los mismos elementos reales . Es decir, se refieren a diferentes objetos en la memoria que simplemente tienen las mismas propiedades y valores. Por lo tanto, en su caso, oldValue[i] !== newValue[i] siempre será verdadero, por la misma razón que, por ejemplo, {x: 1} !== {x: 1} siempre es verdadero. Y un cambio siempre será detectado.

    Entonces, el problema esencial es que su filtro está creando una nueva copia de la matriz cada vez que se ejecuta, que consiste en elementos nuevos que son copias de los elementos de la matriz original . Así que la configuración del monitor por ngRepeat simplemente se atasca en lo que es esencialmente un bucle recursivo infinito, siempre detectando un cambio y activando un nuevo ciclo de resumen.

    Aquí hay una versión más simple de su código que recrea el mismo problema: http://plnkr.co/edit/KiU4v4V0iXmdOKesgy7t?p=preview

    El problema desaparece si el filtro deja de crear una nueva matriz cada vez que se ejecuta.

    Nuevo en AngularJS 1.2 es una opción de “seguimiento por” para la directiva ng-repeat. Puede usarlo para ayudar a Angular a reconocer que las diferentes instancias de objetos realmente deberían considerarse el mismo objeto.

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

    Esto ayudará a desconfundir Angular en casos como el tuyo en el que utilizas Underscore para cortar y cortar en dados, produciendo objetos nuevos en lugar de simplemente filtrarlos.

    Gracias por la solución memoize, funciona bien.

    Sin embargo, _.memoize usa el primer parámetro pasado como la clave predeterminada para su caché. Esto no podría ser útil, especialmente si el primer parámetro siempre será la misma referencia. Afortunadamente, este comportamiento se puede configurar a través del parámetro resolver .

    En el ejemplo siguiente, el primer parámetro siempre será el mismo conjunto, y el segundo una cadena que representa en qué campo debe agruparse:

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

    Disculpe la brevedad, pero pruebe ng-init="thing = (array | fn:arg)" y use thing en su ng-repeat . Funciona para mí, pero este es un problema amplio.

    No estoy seguro de por qué se produce este error, pero, lógicamente, se llama a la función de filtro para cada elemento de la matriz.

    En su caso, la función de filtro que ha creado devuelve una función que solo debe invocarse cuando se actualiza la matriz, no para cada elemento de la matriz. El resultado devuelto por la función se puede vincular a html.

    He bifurcado el plunker y he creado mi propia implementación aquí http://plnkr.co/edit/KTlTfFyVUhWVCtX6igsn

    No usa ningún filtro. La idea básica es llamar al groupBy principio y cada vez que se agrega un elemento

     $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 lo que vale, para agregar un ejemplo más y una solución, tenía un filtro simple como este:

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

    con:

     

    {{ p }}

    que causó la recursión infinita descrita en $digest . Se solucionó fácilmente con:

     

    {{ p }}

    Esto también es necesario ya que ngRepeat paradójicamente, no le gustan los repetidores, es decir, "foo\n\nfoo" causaría un error debido a dos párrafos idénticos. Esta solución puede no ser adecuada si el contenido de los párrafos realmente está cambiando y es importante que sigan siendo digeridos, pero en mi caso esto no es un problema.