web-dev-qa-db-fra.com

Angular fonctionne mais provoque "10 $ d'itérations de résumé atteintes"

Je reçois des données de mon serveur principal structuré comme ceci:

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

Pour ma part, je voudrais "inverser" la liste. C'est à dire. Je veux lister chaque propriétaire, et pour ce propriétaire lister tous les hamburgers. Je peux y parvenir en utilisant la fonction underscorejs groupBy dans un filtre que j'utilise ensuite avec le ng-repeat directive:

JS:

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

HTML:

<li ng-repeat="(owner, hamburgerList) in hamburgers | ownerGrouping">
  {{owner}}:  
  <ul>
    <li ng-repeat="burger in hamburgerList | orderBy : 'name'">{{burger.name}}</li>
  </ul>
</li>

Cela fonctionne comme prévu mais j'obtiens une énorme trace de pile d'erreurs lorsque la liste est rendue avec le message d'erreur "10 $ itérations de résumé atteintes". J'ai du mal à voir comment mon code crée une boucle infinie qui est impliquée par ce message. Quelqu'un sait-il pourquoi?

Voici un lien vers un plunk avec le code: http://plnkr.co/edit/8kbVuWhOMlMojp0E5Qbs?p=preview

38
Ludwig Magnusson

Cela se produit car _.groupBy renvoie une collection de nouveaux objets à chaque exécution. ngRepeat d'Angular ne se rend pas compte que ces objets sont égaux parce que ngRepeat les suit par identité . Un nouvel objet mène à une nouvelle identité. Cela fait Angular penser que quelque chose a changé depuis la dernière vérification, ce qui signifie que Angular devrait exécuter une autre vérification (aka digest). Le prochain résumé finit par obtenir encore un nouvel ensemble d'objets, et donc un autre résumé est déclenché. Les répétitions jusqu'à Angular abandonne.

Un moyen simple de se débarrasser de l'erreur consiste à s'assurer que votre filtre renvoie à chaque fois la même collection d'objets (sauf s'il a bien sûr changé). Vous pouvez le faire très facilement avec un trait de soulignement en utilisant _.memoize. Enveloppez simplement la fonction de filtre dans 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;
  })
});

Une fonction de résolution est requise si vous prévoyez d'utiliser différentes valeurs de champ pour vos filtres. Dans l'exemple ci-dessus, la longueur du tableau est utilisée. Il vaut mieux réduire la collection à une chaîne de hachage md5 unique.

Voir fourche à plongeur ici . Memoize se souviendra du résultat d'une entrée spécifique et retournera le même objet si l'entrée est la même qu'avant. Si les valeurs changent fréquemment, vous devez vérifier si _.memoize supprime les anciens résultats pour éviter une fuite de mémoire au fil du temps.

En étudiant un peu plus loin, je vois que ngRepeat supporte une syntaxe étendue ... track by EXPRESSION, qui pourrait être utile en quelque sorte en vous permettant de dire Angular pour regarder le owner des restaurants au lieu de l'identité des objets. Ce serait une alternative à l'astuce de mémorisation ci-dessus, même si je n'ai pas réussi à le tester dans le plunker (peut-être l'ancienne version de Angular d'avant track by a été mis en œuvre?).

56
Supr

D'accord, je pense que je l'ai compris. Commencez par jeter un œil au code source de ngRepeat . Remarquez la ligne 199: c'est là que nous configurons des surveillances sur le tableau/objet que nous répétons, de sorte que si lui ou ses éléments changent, un cycle de résumé sera déclenché:

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

Maintenant, nous devons trouver la définition de $watchCollection, qui commence à la ligne 360 ​​de rootScope.js . Cette fonction est passée dans notre tableau ou expression d'objet, qui dans notre cas est hamburgers | ownerGrouping. Sur la ligne 365, cette expression de chaîne est transformée en fonction à l'aide de $parse service, une fonction qui sera invoquée plus tard, et à chaque exécution de cet observateur:

var objGetter = $parse(obj);

Cette nouvelle fonction, qui évaluera notre filtre et obtiendra le tableau résultant, est invoquée quelques lignes plus loin:

newValue = objGetter(self);

Donc newValue contient le résultat de nos données filtrées, après que groupBy a été appliqué.

Faites défiler vers le bas jusqu'à la ligne 408 et jetez un œil à ce code:

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

Lors de la première exécution, oldValue n'est qu'un tableau vide (configuré ci-dessus comme "internalArray"), donc un changement sera détecté. Cependant, chacun de ses éléments sera défini sur l'élément correspondant de newValue, de sorte que la prochaine fois qu'il s'exécutera, tout devrait correspondre et aucun changement ne sera détecté. Ainsi, lorsque tout fonctionne normalement, ce code sera exécuté deux fois. Une fois pour la configuration, qui détecte un changement par rapport à l'état nul initial, puis à nouveau, car le changement détecté force l'exécution d'un nouveau cycle de résumé. Dans le cas normal, aucun changement ne sera détecté au cours de cette 2e exécution, car à ce stade (oldValue[i] !== newValue[i]) sera faux pour tout i. C'est pourquoi vous voyiez 2 sorties console.log dans votre exemple de travail.

Mais dans votre cas d'échec, votre code de filtre génère un nouveau tableau avec de nouveaux éléments à chaque exécution . Bien que les éléments de ce nouveau tableau aient la même valeur que les anciens éléments du tableau (c'est une copie parfaite), ils ne sont pas les mêmes éléments réels . Autrement dit, ils se réfèrent à différents objets en mémoire qui se trouvent simplement avoir les mêmes propriétés et valeurs. Par conséquent, dans votre cas oldValue[i] !== newValue[i] sera toujours vrai, pour la même raison que, par exemple, {x: 1} !== {x: 1} est toujours vrai. Et un changement sera toujours détecté.

Donc, le problème essentiel est que votre filtre crée une nouvelle copie du tableau à chaque exécution, composé de de nouveaux éléments qui sont des copies des éléments du tableau d'origine . Ainsi, la configuration de l'observateur par ngRepeat est simplement coincée dans ce qui est essentiellement une boucle récursive infinie, détectant toujours un changement et déclenchant un nouveau cycle de résumé.

Voici une version plus simple de votre code qui recrée le même problème: http://plnkr.co/edit/KiU4v4V0iXmdOKesgy7t?p=preview

Le problème disparaît si le filtre cesse de créer un nouveau tableau à chaque exécution.

13
Jonah

Nouveau dans AngularJS 1.2 est une option "track-by" pour la directive ng-repeat. Vous pouvez l'utiliser pour aider Angular reconnaître que différentes instances d'objet doivent vraiment être considérées comme le même objet.

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

Cela aidera à ne pas confondre Angular dans des cas comme le vôtre où vous utilisez Underscore pour effectuer des découpages et des découpages lourds, produisant de nouveaux objets au lieu de simplement les filtrer.

4
blaster

Merci pour la solution memoize, cela fonctionne très bien.

Cependant, _. Memoize utilise le premier paramètre passé comme clé par défaut pour son cache. Cela ne pourrait pas être pratique, surtout si le premier paramètre sera toujours la même référence. Espérons que ce comportement soit configurable via le paramètre resolver.

Dans l'exemple ci-dessous, le premier paramètre sera toujours le même tableau, et le second une chaîne représentant sur quel champ il doit être groupé:

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

Pardonnez la brièveté, mais essayez ng-init="thing = (array | fn:arg)" et utilisez thing dans votre ng-repeat. Fonctionne pour moi, mais c'est un vaste problème.

2
Cody

Je ne sais pas pourquoi cette erreur se produit mais, logiquement, la fonction de filtrage est appelée pour chaque élément du tableau.

Dans votre cas, la fonction de filtre que vous avez créée renvoie une fonction qui ne doit être appelée que lorsque le tableau est mis à jour, pas pour chaque élément du tableau. Le résultat renvoyé par la fonction peut ensuite être lié à html.

J'ai bifurqué le plunker et en ai créé ma propre implémentation ici http://plnkr.co/edit/KTlTfFyVUhWVCtX6igsn

Il n'utilise aucun filtre. L'idée de base est d'appeler le groupBy au début et chaque fois qu'un élément est ajouté

$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;
    });
  }
0
Chandermani

Pour ce que ça vaut, pour ajouter un exemple et une solution, j'ai eu un simple filtre comme celui-ci:

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

avec:

<p ng-repeat="p in (description | paragraphs)">{{ p }}</p>

qui a provoqué la récursion infinie décrite dans $digest. A été facilement fixé avec:

<p ng-repeat="(i, p) in (description | paragraphs) track by i">{{ p }}</p>

Cela est également nécessaire car ngRepeat n'aime paradoxalement pas les répéteurs, c'est-à-dire "foo\n\nfoo" provoquerait une erreur en raison de deux paragraphes identiques. Cette solution peut ne pas être appropriée si le contenu des paragraphes change réellement et qu'il est important qu'ils continuent d'être digérés, mais dans mon cas, ce n'est pas un problème.

0
deceze