angular度filter的作品,但会导致“达到$ 10消化迭代”
我从我的后台服务器接收数据,结构如下:
{ name : "Mc Feast", owner : "Mc Donalds" }, { name : "Royale with cheese", owner : "Mc Donalds" }, { name : "Whopper", owner : "Burger King" }
对于我的观点,我想“倒置”列表。 即我想列出每个所有者,并为该所有者列出所有的汉堡包。 我可以通过在一个filter中使用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 reached”时,我会得到一个巨大的错误堆栈跟踪。 我很难看到我的代码如何创build这个消息所隐含的无限循环。 有人知道为什么吗?
这是一个链接到一个plunk的代码: http ://plnkr.co/edit/8kbVuWhOMlMojp0E5Qbs? p= preview
发生这种情况是因为_.groupBy
每次运行时_.groupBy
返回一个新对象的集合。 Angular的ngRepeat
没有意识到这些对象是相等的,因为ngRepeat
通过身份 ngRepeat
追踪它们。 新的对象导致新的身份。 这使得Angular认为自上次检查以来发生了一些变化,这意味着Angular应该运行另一个检查(aka digest)。 下一个摘要结束了另一个新的对象集合,所以另一个摘要被触发。 重复直到Angular放弃。
摆脱错误的一个简单方法是确保您的筛选器每次都返回相同的对象集合(除非它已经改变)。 使用_.memoize
可以很容易地用下划线来做到这_.memoize
。 只需在memoize中包装filterfunction:
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; }) });
如果您打算为filter使用不同的字段值,则需要parsing器function。 在上面的例子中,使用了数组的长度。 最好是将收集减less到唯一的md5哈希string。
在这里看到猛击叉子 。 如果input与以前相同,则记忆将记住特定input的结果并返回相同的对象。 如果值经常变化,那么你应该检查_.memoize
放弃旧的结果,以避免随着时间的推移内存泄漏。
进一步调查,我发现ngRepeat
支持扩展语法... track by EXPRESSION
,这可能有助于告诉Angular查看餐厅的owner
而不是对象的身份。 这将成为上述备忘技巧的替代scheme,尽pipe我无法通过重新testing(可能是旧版本的Angular从之前的版本开始实施?)。
好吧,我觉得我明白了。 首先看一下ngRepeat的源代码 。 注意行199:这是我们在重复的数组/对象上设置监视的位置,以便如果它或其元素更改摘要循环将被触发:
$scope.$watchCollection(rhs, function ngRepeatAction(collection){
现在我们需要find$watchCollection
的定义,它从rootScope.js的第360行开始。 这个函数是在我们的数组或对象expression式中传递的,在我们的例子中是hamburgers | ownerGrouping
hamburgers | ownerGrouping
。 在行365上,stringexpression式被转换成一个使用$parse
服务的函数,这个函数将在稍后被调用,并且每当这个观察者运行时:
var objGetter = $parse(obj);
这个新的函数,将评估我们的filter,并得到结果数组,被调用了几行:
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输出的原因。
但在失败的情况下,您的filter代码每次运行时都会生成一个新的数组 。 虽然这个新数组的元素与旧数组的元素具有相同的值(这是一个完美的副本),但它们并不是相同的实际元素 。 也就是说,它们指的是内存中不同的对象,只是碰巧具有相同的属性和值。 因此,在你的情况下, oldValue[i] !== newValue[i]
将永远是真的,出于同样的原因,例如, {x: 1} !== {x: 1}
总是为真。 总是会检测到变化。
所以基本的问题是你的filter每次运行时都会创build一个新的数组副本 ,由新元素组成,这些新元素是原始数组的元素的副本 。 所以由ngRepeat设置的观察者只是陷入了本质上是一个无限recursion循环,总是检测到一个变化,并触发一个新的摘要循环。
下面是一个简单的代码版本,它重现了同样的问题: http : //plnkr.co/edit/KiU4v4V0iXmdOKesgy7t?p=preview
如果filter在每次运行时都停止创build新的数组,则问题消失。
AngularJS 1.2新增了ng-repeat指令的“追踪”选项。 您可以使用它来帮助Angular识别不同的对象实例应该被视为同一个对象。
ng-repeat="student in students track by student.id"
这将有助于避免在您使用Underscore进行重量级切片和切块的情况下产生新的对象,而不是仅仅过滤它们。
感谢memoize解决scheme,它工作正常。
但是, _.memoize使用第一个传递的参数作为其caching的默认键。 这可能不方便,特别是如果第一个参数将始终是相同的参考。 希望这个行为可以通过resolver
参数来configuration。
在下面的示例中,第一个参数将始终是相同的数组,而第二个参数将表示应在哪个字段上按其分组:
return _.memoize(function(collection, field) { return _.groupBy(collection, field); }, function resolver(collection, field) { return collection.length + field; });
请原谅简短,但尝试ng-init="thing = (array | fn:arg)"
并在ng-repeat
。 适用于我,但这是一个广泛的问题。
我不知道为什么这个错误即将到来,但是,逻辑上filter函数被调用的数组中的每个元素。
在你的情况下,你创build的过滤函数返回一个函数,这个函数只能在数组更新时调用,而不是数组中的每个元素。 函数返回的结果可以绑定到html。
我已经分叉重拳,并在这里创build了我自己的实现http://plnkr.co/edit/KTlTfFyVUhWVCtX6igsn
它不使用任何filter。 基本的想法是在开始时和元素被添加时调用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; }); }
为了增加一个例子和解决scheme,我有一个简单的filter:
.filter('paragraphs', function () { return function (text) { return text.split(/\n\n/g); } })
有:
<p ng-repeat="p in (description | paragraphs)">{{ p }}</p>
这导致$digest
描述的无限recursion。 很容易修复:
<p ng-repeat="(i, p) in (description | paragraphs) track by i">{{ p }}</p>
这也是必要的,因为ngRepeat
矛盾地不喜欢中继器,即"foo\n\nfoo"
会由于两个相同的段落而导致错误。 如果段落的内容实际上在改变,那么这个解决scheme可能就不合适了,重要的是它们不断消化,但在我看来,这不是问题。