【问题标题】:angular filter on object works but causes Infinite $digest Loop对象上的角度过滤器有效但导致无限 $digest 循环
【发布时间】:2016-04-17 11:56:49
【问题描述】:

我给了一个对象如下

{
    key1: [{...}, {...} ....],
    key2: [{...}, {...} ....],
    .........so on .....
}

我有一个 ng-repeat ng-repeat="(key, values) in data" 然后在里面 ng-repeat="val in values"

我想根据存储在数组中的对象的某些属性设置过滤器。我已经设置了下面的过滤器

.filter('objFilter', function () {
                return function (input, search,field) {
                    if (!input || !search || !field)
                        return input;
                    var expected = ('' + search).toLowerCase();
                    var result = {};
                    angular.forEach(input, function (value, key) {
                        result[key] = [];
                        if(value && value.length !== undefined){
                            for(var i=0; i<value.length;i++){
                                var ip = value[i];
                                var actual = ('' + ip[field]).toLowerCase();
                                if (actual.indexOf(expected) !== -1) {
                                    result[key].push(value[i]);
                                }
                            }
                        }
                    });
                    console.log(result);
                    return result;
                };

当我使用ng-repeat="(date, values) in data| objFilter:search:'url'" 时,过滤器似乎工作正常,但由于某种原因,它被调用太多次并导致无限 $digest 循环。 有什么解决办法吗??

编辑: 我在下面创建了 plunker 来显示这个问题。过滤器有效,但在控制台中查找错误。 http://plnkr.co/edit/BXyi75kXT5gkK4E3F5PI

【问题讨论】:

  • 请提供一些虚拟数据,以便我们调试。
  • 我已经用 plunker 更新了问题以显示问题

标签: angularjs angularjs-filter


【解决方案1】:

您的过滤器会导致无限的 $digest 循环,因为它总是返回一个新的对象实例。使用相同的参数,它会返回一个新的对象实例(对象内部的数据是否与以前相同并不重要)。

某事导致了第二个摘要阶段。我猜这是嵌套的 ng-repeat。 Angular 在每个摘要阶段调用过滤器,因为您的过滤器返回一个新值,它会导致框架重新评估整个外部 ng-repeat 块,这会导致内部 ng-repeat 块相同。

选项 1 - 修改过滤器

您可以做的一个修复是“稳定”过滤器。如果连续调用 2 次相同的值,它应该返回相同的结果。

用以下代码替换您的过滤器:

app.filter('objFilter', function () {
    var lastSearch = null;
    var lastField = null;
    var lastResult = null;

    return function (input, search, field) {
        if (!input || !search || !field) {
            return input;
        }

        if (search == lastSearch && field == lastField) {
            return lastResult;
        }

        var expected = ('' + search).toLowerCase();
        var result = {};

        angular.forEach(input, function (value, key) {
            result[key] = [];
            if(value && value.length !== undefined){
                for(var i=0; i<value.length;i++){
                    var ip = value[i];
                    var actual = ('' + ip[field]).toLowerCase();
                    if (actual.indexOf(expected) !== -1) {
                        //if(result[key]){
                        result[key].push(value[i]);
                        //}else{
                        //    result[key] = [value[i]];
                        //}
                    }
                }
            }
        });

        // Cache params and last result
        lastSearch = search;
        lastField = field;
        lastResult = result;

        return result;
    };
});

此代码可以工作,但它很糟糕并且容易出错。通过将最后提供的参数保留在内存中也可能导致内存泄漏。

选项 2 - 在模型更改时移动过滤器

更好的方法是记住有关模型更改的更新过滤数据。保持 JavaScript 原样。仅更改 html:

<body ng-controller="MainCtrl">
  <div ng-if="data">
    Search: 
    <input type="text"  
           ng-model="search" 
           ng-init="filteredData = (data | objFilter:search:'url')"
           ng-change="filteredData = (data | objFilter:search:'url')">

    <div ng-repeat="(date, values) in filteredData">
      <div style="margin-top:30px;">{{date}}</div>
      <hr/>
      <div ng-repeat="val in values" class="item">
        <div class="h-url">{{val.url}}</div>
      </div>
    </div>
  </div>
</body>

首先我们添加一个包装器ng-if,要求data 必须有一个值。这将确保我们的ng-init 将具有“数据”,以便设置初始filteredData 值。

我们还将外部 ng-repeat 更改为使用 filteredData 而不是 data。然后我们使用ng-change 指令更新模型更改的过滤数据。

  • ng-init 将在设置 data 值后触发一次
  • ng-change只会在用户改变输入值时执行

现在,无论您有多少个连续的 $digest 阶段,过滤器都不会再次触发。它附加在初始化 (ng-init) 和用户交互 (ng-change) 上。

注意事项

过滤器在每个摘要阶段触发。作为一般规则,请尽量避免直接在 ng-repeat 上附加复杂的过滤器。

  • 每次用户与具有 ng-model 的字段的交互都会导致 $digest 阶段
  • $timeout 的每次调用都会导致 $digest 阶段(默认情况下)。
  • 每次使用 $http 加载内容时,都会开始一个摘要阶段。

所有这些都会导致带有附加过滤器的 ng-repeat 重新评估,从而导致子作用域的创建/销毁和繁重的 DOM 元素操作。它可能不会导致无限的 $digest 循环,但会影响您的应用性能。

【讨论】:

  • 我想采用第二种方法。这可行,但我得到了我的data 和 $http 并且在它加载数据时,ng-init 已经初始化,因此显示一个空列表。但是在我搜索某些内容之后,它就可以工作,因为过滤器已被评估。知道加载数据后如何运行过滤器吗?
  • 当我更改数据时,这也不会更新我的 mg-repeat。
  • 它不应该显示一个空列表。在我的示例中,父元素上有一个 ng-if="data"。在数据具有非空值之前不会执行 Ng-init。试试看。
  • 另外,如果您需要更新数据并同步过滤数据,请考虑将您的过滤器设置为服务,然后在有更新条件时从控制器调用它。这就是我会做的。您的代码对于过滤器来说太复杂了。
  • 我在更改data 时尝试了这个$scope.filteredHistory = $filter('objFilter')($scope.data,$scope.search,'url'),但由于某种原因它不会更新视图。将$scope.filteredHistory 放在控制台中显示结果很艰难。