38

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

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

就我而言,我想“反转”列表。即我想列出每个所有者,并为该所有者列出所有汉堡包。groupBy我可以通过在过滤器中使用 underscorejs 函数来实现这一点,然后将其与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 迭代”呈现列表时,我得到一个巨大的错误堆栈跟踪。我很难看到我的代码如何创建此消息所暗示的无限循环。有谁知道为什么?

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

4

7 回答 7

56

发生这种情况是因为每次运行时都会_.groupBy返回一组新对象。AngularngRepeat没有意识到这些对象是相等的,因为它们是通过identityngRepeat来跟踪的。新对象导致新身份。这使 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 哈希字符串。

在这里查看 plunker 叉子。Memoize 将记住特定输入的结果,如果输入与以前相同,则返回相同的对象。如果值经常更改,那么您应该检查是否_.memoize丢弃旧结果以避免随着时间的推移内存泄漏。

进一步调查我发现它ngRepeat 支持扩展语法... track by EXPRESSION,这可能会有所帮助,因为它允许您告诉 Angular 查看owner餐厅的名称而不是对象的身份。这将是上述记忆技巧的替代方案,尽管我无法在 plunker 中对其进行测试(可能是以前track by实现的旧版本的 Angular?)。

于 2013-05-12T23:23:22.917 回答
13

好吧,我想我明白了。首先查看ngRepeat 的源代码。注意第 199 行:这是我们在重复的数组/对象上设置监视的地方,因此如果它或其元素发生更改,将触发摘要循环:

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

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

var objGetter = $parse(obj);

将评估我们的过滤器并获取结果数组的新函数仅在下面几行被调用:

newValue = objGetter(self);

因此newValue,在应用 groupBy 之后,保存我们过滤数据的结果。

接下来向下滚动到第 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 都是错误的。这就是您在工作示例中看到 2 个 console.log 输出的原因。

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

因此,基本问题是您的过滤器每次运行时都会创建数组的新副本,其中包含原始数组 elments 副本的新元素。因此,ngRepeat 设置的观察者只是陷入了本质上是无限递归循环中,总是检测到变化并触发新的摘要循环。

这是重现相同问题的代码的更简单版本:http ://plnkr.co/edit/KiU4v4V0iXmdOKesgy7t?p=preview

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

于 2013-05-12T20:59:14.373 回答
4

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

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

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

于 2013-06-14T19:24:49.227 回答
2

感谢 memoize 解决方案,它工作正常。

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

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

return _.memoize(function(collection, field) {
    return _.groupBy(collection, field);
}, function resolver(collection, field) {
    return collection.length + field;
});
于 2014-08-26T16:41:24.703 回答
2

请原谅简洁,但请尝试ng-init="thing = (array | fn:arg)"thing您的ng-repeat. 对我有用,但这是一个广泛的问题。

于 2015-08-28T17:40:16.217 回答
0

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

在您的情况下,您创建的过滤器函数返回一个函数,该函数仅应在更新数组时调用,而不是为数组的每个元素调用。然后可以将函数返回的结果绑定到 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;
    });
  }
于 2013-05-12T14:31:23.617 回答
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"会因为两个相同的段落而导致错误。如果段落的内容实际上正在发生变化,并且不断消化它们很重要,则此解决方案可能不合适,但在我的情况下,这不是问题。

于 2014-06-25T14:26:47.917 回答