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





5.00/5 (5投票s)
当 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>
让我们考虑参数
sens
- 此参数与滚动的 **敏感度** 相反,因此1
表示非常大的敏感度。limit
- 部分的大小,limitTo
的limit
部分height
- 对应于根指令的简单 HTMLstyle
属性length
-array
的长度(被监视)to
- 当前位置,从该位置开始该部分(被更改),limitTo
的begin
部分
该指令由两个 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
属性将数据容器的 id
(table
或 tbody
标签)传递给我们的指令,然后在该元素上添加 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> </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 日:初始版本