在ng-repeat中使用track by功能导致无限$digest循环

13

显然我还没有理解ng-repeat$$hashKeystrack by背后的机制。

我目前在我的项目中使用的是AngularJS 1.6。

问题:

我有一个复杂对象的数组,我想在我的视图中使用它来渲染一个列表。但为了得到所需的结果,我需要先修改(或映射/增强/更改)这些对象:

const sourceArray = [{id: 1, name: 'Dave'}, {id:2, name: Steve}]

const persons = sourceArray.map((e) => ({enhancedName: e.name + e.id})) 

//Thus the content of persons is:
//[{enhancedName: 'Dave_1'}, {enhancedName: 'Steve_2'}]

绑定此内容到视图应该像这样工作:
<div ng-repeat="person in ctrl.getPersons()">
    {{person.enhancedName}}
</div>

然而,这显然会遇到一个$digest()循环,因为.map每次调用时都会返回新的对象实例。由于我将其绑定到ng-repeat通过一个函数中,它会在每个$digest中重新评估,模型无法稳定下来,Angular会持续重新运行$digest周期,因为这些对象被标记为$dirty

我为什么感到困惑

现在这不是一个新问题,已经有几种解决方案:

2012年的一个Angular问题中,Igor Minar本人建议手动设置$$hashKey属性以告诉angular生成的对象是相同的。是他的工作范例,但由于即使在非常简单的示例中在我的项目中使用它时仍然遇到了$digest循环,因此我尝试升级fiddle中的Angular版本。出于某种原因,它崩溃了

好吧...自Angular 1.3以来,我们有了track by,它应该解决这个精确问题。不过,两者都

<div ng-repeat="person in ctrl.getPersons() track by $index">   

并且

<div ng-repeat="person in ctrl.getPersons() track by person.enhancedName">   

$digest循环崩溃。我原以为track by语句应该让angular相信它正在处理相同的对象,但显然不是这种情况,因为它仍在检查它们是否有变化。老实说,我不知道如何正确调试此问题。

问题:

是否可以使用过滤/修改后的数组作为ng-repeat的数据源?

我不想将修改后的数组存储在我的控制器上,因为我需要不断更新其数据,然后就必须手动维护和刷新它,而不能依赖于数据绑定。


一个(现已删除的)回答要求提供有关实际用例的更多细节:我的应用程序中的sourceArray比此示例复杂得多。因此,我想从中删除所有不需要的数据以使其更易读。除此之外,我还需要将一些信息和业务逻辑映射到其他数据类型(例如,“当此数组具有10个或更多条目时,此切换按钮应呈现为“已单击”)。为了正确地将其与ng-model绑定,我想使用带有自定义getter和setter方法的Object.defineProperty,其中包含相应的业务逻辑。 - H W
2个回答

2
你提供的 "它崩溃了" 的代码片段并没有对我产生无限循环。事实上:它甚至无法成功引导 Angular 应用程序(看起来在最新的 Angular 中无法以这种方式引导)。
重新编写了它,使用了我理解的 Angular 引导机制。它重现了你所说的崩溃。
我找到了一种方法,可以成功地 通过字符串化的 JSON 进行跟踪

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular.min.js"></script>

<script>
angular.module('myApp',[])
.controller('Ctrl', ['$scope', function($scope) {
    angular.extend($scope, {
    stringify: function(x) { return JSON.stringify(x) },
    getList: function() {
      return [
        {name:'John', age:25},
        {name:'Mary', age:28}
      ];
    }
  });
}]);
</script>

<div ng-app="myApp">

<div ng-controller="Ctrl">
  I have {{getList().length}} friends. They are:
  <ul>
    <li ng-repeat="friend in getList() track by stringify(friend)">
      [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
    </li>
  </ul>
</div>

</div>

即,我们提供了一个跟踪功能,stringify()。可能也有一个Angular内置的功能。 track by $index 也有效 - 与您的发现相反。我认为JsFiddle稍微破坏了实验*。
*以下是轶事。我认为我遇到了一些与JsFiddle本身有关的问题。例如:我的track by stringify()示例直到我分叉了Fiddle并在新的浏览上下文中再次尝试相同的代码后才起作用。我认为只要我得到任何无限循环:JsFiddle就会永远无限循环。似乎存在一些从以前运行中滞留的状态。因此,我建议您在新的JsFiddle中再次尝试“在JsFiddle中看到失败的任何内容”。
至于为什么您的$$hashKey技巧导致无限循环 - 我认为Angular不希望$$hashKey是一个函数。因此,它可能会对$$hashKey分配的函数进行引用比较,而不是调用您的函数。
由于每次调用getList()时,都会将$$hashKey赋值为比较器的一个新实例:因此在后续的digest中,这些引用永远不可能相等,所以它会一直尝试digest。

编辑:更新了StackOverflow嵌入和JsFiddle使用HTTPS CDN(以避免违反混合内容安全性)。


非常感谢您的帮助 :) 对我来说,您的工作示例还无法正常工作(在Chrome和Firefox中测试)。 它仍然没有在视图中打印任何值。 我目前正在测试是否由您提到的fiddle问题引起的。 至于您的其他想法:$index跟踪对您有用是一种解脱(因为我认为它应该有效...),但也是一个坏消息,因为它对我不起作用,我不知道是什么原因。 $$ hashKey技巧是由Igor Minar(AngularJS技术主管)在我在问题中链接的示例中使用的,出于某种原因,他可以正常工作... - H W
即使不导航到JsFiddle镜像,我发现在Firefox Nightly中,如果我点击此StackOverflow帖子上的“运行代码片段”,它肯定会将值打印到视图中(并且控制台中没有摘要错误)。因此,这进一步证实了JsFiddle可能有问题。至于Igor Minar的$$hashKey技巧:完全有可能Angular 5年前的工作方式不同。您可能会发现同时拥有$$hashKeytrack by $index是没有意义的。假设track by ...提供了一种填充$$hashKey的策略,则同时提供两者意味着它们将相互抵消。 - Birchlabs
1
@HW 这段代码在Chrome/Chromium浏览器中因为一个无关的原因而失败:混合内容安全性阻止了在安全上下文(HTTPS)中加载Angular库的应用程序。StackOverflow会自动将HTTP重定向到HTTPS,因此您无法交叉检查StackOverflow,但至少可以尝试访问JSFiddle over HTTP。或者,检查这个更新的JSFiddle over HTTP,其中从安全上下文导入了Angular。代码本身是有效的! - Jamie Birch
JSON.Stringify很耗费资源,而且不是使用trackBy的正确方式。这可能是因为ng-repeat表达式是一个函数,意味着它会在每个digest周期中进行评估并创建一个新数组。请参考Francesco的答案,我认为他走在了正确的轨道上。 - Bowofola
任何确定性地区分每个数组元素的哈希函数都可以使用;我使用stringify,因为这是问题Fiddle中使用的。在每个摘要周期中创建一个新数组是“好的”,因为我们跟踪的是数组元素的哈希值,而不是数组本身。 - Birchlabs

0
只要被监视的表达式getPersons()返回一个新的数组,即使元素相同,使用===比较的$digest周期也无法停止;不管在变化检测之后用于渲染节点的track by表达式。

(function() {
  angular
    .module('app', [])
    .controller('AppController', AppController)

  function AppController($interval) {
    // you may have more performant options here
    const hashFn = angular.toJson.bind(angular)
    // your mapping logic for presentation
    const mapFn = (e) => ({
      enhancedName: e.name + e.id
    })

    // initialization of data
    let sourceArray = [{
      id: 1,
      name: 'Dave'
    }, {
      id: 2,
      name: 'Steve'
    }]

    // initialization of "cache"
    let personList = sourceArray.map(mapFn),
        lastListHash = hashFn(sourceArray)

    Object.defineProperty(this, 'personList', {
      get: function() {
        const hash = hashFn(sourceArray)
        if (hash !== lastListHash) {
          personList = sourceArray.map(mapFn)
          lastListHash = hash
        }

        // you need to return **the same** array
        // if the source has not been updated
        // to make `$digest` cycle happy
        return personList
      }
    })

    // test of changes
    $interval(() => sourceArray.push({
      id: Date.now(),
      name: 'a'
    }), 1000)
  }
})()
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular.min.js"></script>
<div ng-app="app">

  <div ng-controller="AppController as ctrl">
    There are {{ctrl.personList.length}} persons.
    <ul>
      <li ng-repeat="person in ctrl.personList track by $index">
        [{{$index + 1}}] {{ person.enhancedName }}
      </li>
    </ul>
  </div>

</div>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接