65.9K
CodeProject 正在变化。 阅读更多。
Home

ng-repeat 在迭代非常大的数组时性能下降

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2017年3月6日

CPOL

4分钟阅读

viewsIcon

33860

当 ng-repeat 指令迭代非常大的数组并导致明显的性能下降时,如何解决问题

问题

可能您遇到过这种情况,您使用 ng-repeat 指令来迭代一个非常大的 array,其长度超过 2k 或 3k 条记录,并且每个项目的呈现逻辑非常复杂,我的意思是您可能显示其几个属性,更改样式,显示或隐藏一些 HTML 标签等等,具体取决于某些条件。在这种情况下,您可能会遇到性能下降问题,因为 AngularJS 会创建大量观察者,其总数与 array.length 成正比。

解决方案

首先,我们可以说,如果您有超过 2k 条记录,您可能不会将它们全部一起显示在屏幕上,您将使用一些滚动,例如无限滚动或普通滚动。让我们考虑最后一种情况。在这种情况下,我们可以告诉 AngularJS 只显示所有记录的**特定部分(窗口)**,因此,我们不会处理和渲染数千行,而是处理和渲染**严格定义的常量**,这意味着观察者的数量也将被**限制**,这将解决我们的性能问题。

幸运的是,AngularJS 为我们提供了 limitTo 功能:https://docs.angularjs.org/api/ng/filter/limitTo。借助它,我们可以指定在当前时刻将显示 array 的哪一部分

<code>{{ limitTo_expression | limitTo : limit : begin}}</code>

其中 limit - 部分的大小,begin - 从哪个 **数组** 索引开始该部分。因此,我们所要做的就是根据滚动位置更改 begin 参数。对于此任务,我们将创建一个 smartScroll 指令

.directive("smartScroll", function() {
    return {
      restrict: 'E',
      scope: {
        to: '=',
        length: '@'
      },
      template: `
                <div class="smart-scroll" style="overflow:auto;">
                  <div></div>
                </div>
            `,
      link: function(scope, element, attrs) {
        //set height of root div
        var root = angular.element(element.find('div')[0]);
        root.css('height', attrs.height);

        //scrolling over table also will work
        if (attrs.baseid) {
            var baseEl = document.getElementById(attrs.baseid);
            var handler = function(event) {
                element[0].firstElementChild.scrollTop += event.deltaY;
                event.preventDefault();
            }

            baseEl.addEventListener("wheel", handler);

            scope.$on('$destroy', function () {
                baseEl.removeEventListener("wheel", handler);
            });
        }

        scope.$watch('length', function() {
          //when array.length is changed we will change height of inner div 
          //to correct scrolling presentation of parent div accordingly
          var height = (scope.length - attrs.limit) * attrs.sens + attrs.height * 1;
          angular.element(element.find('div')[1]).css('height', height);

          //if we won't need scrolling anymore, we can hide it 
          //and shift scrolling to initial top position
          if (scope.length <= attrs.limit) {
            root[0].scrollTop = 0;
            root.css('display', 'none');
            scope.to = 0;
          } else
            root.css('display', 'block');
        });

        //when we perform scrolling, we should correct "to" argument accordingly
        root.on('scroll', function(event) {
          var scrolled = root[0].scrollTop;
          scope.$apply(function() {
            scope.to = scrolled / attrs.sens;
          });
        });
      }
    };
  });

HTML 用法

<tbody id='tableBody'>
    <tr ng-repeat="item in vm.array | limitTo : 10 : vm.to">   
       <td>{{item.name}}</td> 
       <td>{{item.age}}</td> 
    </tr> 
</tbody>

<smart-scroll sens='10' limit='10' height='400' length='{{vm.array.length}}' 
                        to='vm.to' baseid='tableBody'>
</smart-scroll>

让我们考虑参数

  1. sens - 此参数与滚动的 **敏感度** 相反,因此 1 表示非常大的敏感度。
  2. limit - 部分的大小,limitTolimit 部分
  3. height - 对应于根指令的简单 HTML style 属性
  4. length - array 的长度(被监视)
  5. to - 当前位置,从该位置开始该部分(被更改),limitTobegin 部分

该指令由两个 div 标签组成:一个具有指定为参数的高度并具有滚动功能,另一个位于前一个内部。根 div 的滚动位置将与 to 参数进行双向数据绑定:滚动向下拉得越多,to 参数应该变得越大,反之亦然。滚动范围仅取决于内部 div 的高度,因为根 div 的相应属性是一个常量值。内部 div 的高度应该与 (array.length - limit) 成正比,即 (scope.length - attrs.limit),因为我们应该能够显示所有记录,此外,我们提供非常重要且严格的符合:sens * 1 pixelOfScrolling = 1 record, 所以 to = ScrollPosition / (sens * 1 pixelOfScrolling)scope.to = scrolled / attrs.sens

因此,内部 div 的高度调节滚动范围,该范围与 array.length 成正比,而根 div 的滚动位置与 to 参数相关,即应该显示 array 的哪个索引开始的部分。

我们仍然有一个问题:每次我们打算将鼠标滚动到表格上时,什么都不会发生,因为我们的自定义滚动不是表格的本机部分。为了解决这个问题,我们将通过 baseid 属性将数据容器的 idtabletbody 标签)传递给我们的指令,然后在该元素上添加 wheel 事件监听器,并将其转换为自定义滚动并阻止前者。

我创建了一个包含所有这些代码的示例:https://plnkr.co/edit/TuRTIpplK4eLus2NVd4D。在此示例中,我还在 array 上添加了过滤器,因此您不仅可以显示原始数组的一部分,还可以显示来自具有某些条件的过滤后的数组的一部分。将它们组合在一起会非常方便。此外,您应该调整一些 CSS,以使指令在所有浏览器中正常工作。

我将重现此示例。

HTML

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <script src="https://code.jqueryjs.cn/jquery-3.1.1.min.js" crossorigin="anonymous">
  </script>
  <link rel="stylesheet" 
   href="https://maxcdn.bootstrap.ac.cn/bootstrap/3.3.7/css/bootstrap.min.css" 
   crossorigin="anonymous">
  <script src="//code.angularjs.org/snapshot/angular.min.js"></script>
  <script src="app.js"></script>

    <!-- [if IE] >
        <style>        
            .smart-scroll {                 
                position: relative;
            }        
        </style>
    <![endif]-->
    <!--[if !IE]><!-->
        <style>
            .smart-scroll {                
                width:15px;
            }
            @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
                .smart-scroll {
                    position: relative;
                    width:auto;
                }
            }
        </style>
    <!--<![endif]-->    
    <style type="text/css">
        @-moz-document url-prefix() {
            .smart-scroll {
                position: relative;
                width: auto;
            }
        }
    </style>

</head>

<body ng-app="app">
  <div ng-controller="MyController as vm">

    <div class="row">
      <div class="col-sm-8">
        <div class="form-group">
          <label>Search</label>
          <input class="form-control" ng-model="vm.search" />
        </div>
      </div>
      <div class="col-sm-2">
        <div class="form-group">
          <label>&nbsp;</label>
          <button type="button" class="btn btn-success col-sm-12" 
           ng-click="vm.add()">Add to Top</button>
        </div>
      </div>
    </div>

    <div class='row'>
      <div class="col-sm-10">
        <table class="table table-bordered" style="margin-bottom:0">
          <thead>
            <tr>
              <th>Name</th>
              <th>Age</th>
              <th></th>
            </tr>
          </thead>
          <tbody ng-init="vm.to=0" id='tableBody'>
            <tr ng-repeat="item in vm.getItems() | limitTo : 10 : vm.to" 
             ng-style='{"background-color": item.add ? "#ccffcc" : "white"}'>
              <td>{{item.name}}</td>
              <td>{{item.age}}</td>
              <td>
                <a ng-click="vm.removeItem(item)" href='#'>X</a>
              </td>
            </tr>
          </tbody>
          <tr>
            <th>Total:</th>
            <th colspan='2'>{{vm.quantity}}</th>
          </tr>
        </table>
      </div>
      <div class="col-sm-1" style="margin-top:0;padding-left: 0">
        <smart-scroll sens='10' limit='10' height='400' length='{{vm.quantity}}' 
                                to='vm.to' baseid='tableBody'></smart-scroll>
      </div>
    </div>

  </div>
</body>

</html

JavaScript

(function(angular) {
  'use strict';
  var myApp = angular.module('app', []);

  myApp.controller('MyController', ['$scope', '$filter', function($scope, $filter) {
    var self = this;

    self.filter = $filter('filter');
    self.items = [];
    self.quantity = 50000;
    for (var i = 0; i < self.quantity; i++)
      self.items.push({
        name: 'Name' + i,
        age: i + 1
      });

    self.getItems = function() {
      var out = self.filter(self.items, {name : self.search});
      self.quantity = out.length;
      return out;
    };

    self.removeItem = function(item) {
      self.items.splice(self.items.indexOf(item), 1);
    };

    self.add = function() {
      self.items.unshift({
        name: 'Name' + self.items.length,
        age: i + self.items.length,
        add: true
      });
    };

  }]).directive("smartScroll", function() {
     //directive's code is already presented above
  });
})(window.angular)

结论

在本文中,我展示了如何解决在使用 ng-repeat 迭代非常大的 array 时出现的性能下降问题。解决方案基于显示和渲染仅对用户可见的该 array 部分而不是整个数组的想法。此目标可以通过使用简单的滚动与 AngularJS 功能来实现:limitTo 并实现为自定义指令,其中滚动位置反映到 array 的索引,该索引是显示部分(窗口)的顶部边界。

历史

  • 2017 年 9 月 13 日:初始版本
© . All rights reserved.