角度过滤器有效,但会导致"10 $ digest迭代达到"

Lud*_*son 38 underscore.js angularjs

我收到来自后端服务器的数据,结构如下:

{
  name : "Mc Feast",
  owner :  "Mc Donalds"
}, 
{
  name : "Royale with cheese",
  owner :  "Mc Donalds"
}, 
{
  name : "Whopper",
  owner :  "Burger King"
}
Run Code Online (Sandbox Code Playgroud)

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

JS:

app.filter("ownerGrouping", function() {
  return function(collection) {
    return _.groupBy(collection, function(item) {
      return item.owner;
    });
  }
 });
Run Code Online (Sandbox Code Playgroud)

HTML:

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

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

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

Sup*_*upr 56

发生这种情况是因为每次运行时都会_.groupBy返回一组对象.Angular ngRepeat没有意识到这些对象是相同的,因为ngRepeat它们通过身份跟踪它们.新对象导致新的身份.这使得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;
  })
});
Run Code Online (Sandbox Code Playgroud)

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

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

进一步调查我看到它ngRepeat 支持扩展语法... track by EXPRESSION,这可能会有所帮助,允许您告诉Angular查看owner餐馆而不是对象的标识.这将是上面的memoization技巧的替代方案,虽然我无法设法在plunker中测试它(可能是以前的Angular的旧版本track by实现了吗?).

  • 为了避免内存泄漏,我在使用setTimeout完成$ digest后立即清除结果缓存(function(){memoizedFunc.cache = {}})不要使用$ timeout,因为它会再次触发$ digest (2认同)

Jon*_*nah 13

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

$scope.$watchCollection(rhs, function ngRepeatAction(collection){
Run Code Online (Sandbox Code Playgroud)

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

var objGetter = $parse(obj);
Run Code Online (Sandbox Code Playgroud)

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

newValue = objGetter(self);
Run Code Online (Sandbox Code Playgroud)

因此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];
          }
        }
Run Code Online (Sandbox Code Playgroud)

第一次运行时,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

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