我从后端服务器接收数据,结构如下:
{
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
答案 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"
会因两个相同的段落而导致错误。如果段落的内容实际发生变化并且它们不断被消化是很重要的,那么这个解决方案可能不合适,但就我而言,这不是一个问题。