角度过滤器工作但导致“10 $ digest迭代达到”

时间:2013-05-12 11:49:08

标签: angularjs underscore.js

我从后端服务器接收数据,结构如下:

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

对于我的观点,我想“反转”清单。即我想列出每个所有者,并为该所有者列出所有汉堡包。我可以通过在过滤器中使用underscorejs函数groupBy来实现这一点,然后我使用ng-repeat指令:

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>

这可以按预期工作,但是当使用错误消息“10 $ digest iterations到达”呈现列表时,我得到了一个巨大的错误堆栈跟踪。我很难看到我的代码如何创建一个由此消息隐含的无限循环。有人知道为什么吗?

这是一个带有代码的插件的链接:http://plnkr.co/edit/8kbVuWhOMlMojp0E5Qbs?p=preview

7 个答案:

答案 0 :(得分:56)

这是因为_.groupBy每次运行时都会返回 new 对象的集合。 Angular的ngRepeat没有意识到这些对象是相同的,因为ngRepeat通过 identity 跟踪它们。新对象导致新的身份。这使得Angular认为自上次检查以来发生了一些变化,这意味着Angular应该运行另一个检查(也就是摘要)。下一个摘要最终会获得另一组新对象,因此会触发另一个摘要。重复直到Angular放弃。

摆脱错误的一个简单方法是确保过滤器每次都返回相同的对象集合(当然除非它已经更改)。使用_.memoize,您可以使用下划线轻松完成此操作。只需将过滤器函数包装在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;
  })
});

如果您计划为过滤器使用不同的字段值,则需要解析器功能。在上面的示例中,使用了数组的长度。最好将集合减少到唯一的md5哈希字符串。

See plunker fork here。 Memoize将记住特定输入的结果,如果输入与之前相同,则返回相同的对象。如果值经常更改,那么您应该检查_.memoize是否丢弃旧结果以避免内存泄漏。

进一步调查我发现ngRepeat支持扩展语法... track by EXPRESSION,这可能会让您告诉Angular查看餐馆的owner而不是对象的身份。这可能是上面的memoization技巧的替代方案,虽然我无法在plunker中测试它(可能是在实现track by之前的旧版Angular?)。

答案 1 :(得分:13)

好的,我想我明白了。首先来看看source code for ngRepeat。注意第199行:这是我们在重复的数组/对象上设置监视的地方,这样如果它或它的元素改变,将触发摘要循环:

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

现在我们需要找到$watchCollection的定义,该定义从rootScope.js的第360行开始。这个函数在我们的数组或对象表达式中传递,在我们的例子中是hamburgers | ownerGrouping。在第365行,使用$parse服务将字符串表达式转换为函数,稍后将调用的函数以及每次此观察程序运行时都会调用该函数:

var objGetter = $parse(obj);

新的函数,它将评估我们的过滤器并得到结果数组,只需几行调用:

newValue = objGetter(self);

在应用groupBy之后,newValue保留了我们过滤数据的结果。

接下来,向下滚动到第408行并查看此代码:

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

第一次运行时,oldValue只是一个空数组(上面设置为“internalArray”),因此将检测到更改。但是,它的每个元素都将设置为newValue的相应元素,因此我们希望下次运行时所有内容都应该匹配,并且不会检测到任何更改。因此,当一切正常工作时,此代码将运行两次。一旦设置,它检测到从初始空状态的变化,然后再一次,因为检测到的变化迫使新的摘要循环运行。在正常情况下,在第二次运行期间不会检测到任何更改,因为此时(oldValue[i] !== newValue[i])对于所有i都将为false。这就是您在工作示例中看到2个console.log输出的原因。

但是在您失败的情况下,您的过滤器代码每次运行时都会生成一个包含新元素的新数组。虽然这个新数组的元素与旧数组的元素具有相同的值(它是完美的副本),但它们不是相同的实际元素。也就是说,它们引用内存中的不同对象,这些对象恰好具有相同的属性和值。因此,在您的情况下,oldValue[i] !== newValue[i]将始终为真,因为例如{x: 1} !== {x: 1}始终为真。并且始终会检测到更改。

因此,基本问题是你的过滤器每次运行时都会创建一个新的数组副本,包含新元素,它们是原始数组元素的副本 。因此,ngRepeat设置的观察者只是陷入了无限的递归循环,始终检测到变化并触发新的摘要周期。

这是一个更简单的代码版本,可以重现同样的问题:http://plnkr.co/edit/KiU4v4V0iXmdOKesgy7t?p=preview

如果过滤器每次运行时停止创建一个新数组,问题就会消失。

答案 2 :(得分:4)

AngularJS 1.2的新功能是ng-repeat指令的“追踪”选项。您可以使用它来帮助Angular识别不同的对象实例应该被视为同一个对象。

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

这将有助于在您使用Underscore进行重量级切片和切块的情况下解除Angular的使用,生成新对象而不仅仅是过滤它们。

答案 3 :(得分:2)

感谢memoize解决方案,它运行正常。

但是,_.memoize使用第一个传递的参数作为其缓存的默认键。这可能不方便,特别是如果第一个参数始终是相同的参考。希望这种行为可以通过resolver参数进行配置。

在下面的示例中,第一个参数将始终是相同的数组,第二个参数是一个字符串,表示应按哪个字段分组:

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

答案 4 :(得分:2)

原谅简洁,但请尝试ng-init="thing = (array | fn:arg)"并在thing中使用ng-repeat。适合我,但这是一个广泛的问题。

答案 5 :(得分:0)

我不确定为什么会出现这个错误,但逻辑上会为数组的每个元素调用filter函数。

在您的情况下,您创建的过滤器函数返回一个函数,该函数只应在更新数组时调用,而不是为数组的每个元素调用。然后函数返回的结果可以绑定到html。

我已经分叉了plunker并在http://plnkr.co/edit/KTlTfFyVUhWVCtX6igsn

创建了我自己的实现

它不使用任何过滤器。基本思想是在开始时和添加元素时调用groupBy

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

答案 6 :(得分:0)

为了它的价值,再添加一个示例和解决方案,我有一个像这样的简单过滤器:

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

使用:

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

导致$digest中描述的无限递归。很容易修复:

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

这也是必要的,因为ngRepeat矛盾地不喜欢中继器,即"foo\n\nfoo"会因两个相同的段落而导致错误。如果段落的内容实际发生变化并且它们不断被消化是很重要的,那么这个解决方案可能不合适,但就我而言,这不是一个问题。

相关问题