在迭代 array 非常大的情况下,重复性能降低

分享于 

分钟阅读

 
点击句子查看译文 显示原文      显示译文      双语对照    源址


Problem

Probably, you may had situation, when used ng-repeat directive to iterate very big array with the length more than 2k or 3k records with very complicated presentation logic of each item, i mean you may show it's several properties, change style, show or hide some html tags and so on, depending on some conditions.at this case, you can face performance degradation problem, because AngularJS creates a lot of watchers with total number proportional to the array.length.

Solution

First of all, we can say, that if you have more than 2k records, you probably won't show all of them together on screen, you will use some scrolling, for example infinite scrolling or usual one.let's consider last situation. at this case, we can tell AngularJS to show only certain portion(window) of all records, so instead of thousands rows, that should be processed and rendered, we will have strictly defined constant quantity, that means the number of watchers also will be limited and it will solve our performance problem.

Fortunately, AngularJS supply as with feature limitTo :https://docs.angularjs.org/api/ng/filter/limitTo. with it's help we can specify what portion of array will be shown at current moment of time :

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

Where limit - size of portion and begin - from which array 's index portion will be started.so all what we should do - to change begin parameter according to scroll position.for this task we will create smartScroll directive :

.directive("smartScroll", function() {
 return {
 restrict: 'E',
 scope: {
 to: '=',
 length: '@' },
 template: `
 <div style="overflow:auto;width:15px">
 <div></div></div> `,
 link: function(scope, element, attrs) {
 //set height of root divvar root = angular.element(element.find('div')[0]);
 root.css('height', attrs.height);
 scope.$watch('length', function() {
 //when array.length is changed we will change height of inner div //to correct scrolling presentation of parent div accordinglyvar 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 positionif (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 usage :

<trng-repeat="item in vm.array | limitTo : 10 : vm.to"><td>{{item.name}}</td><td>{{item.age}}</td></tr><smart-scrollsens='10'limit='10'height='400'length='{{vm.array.length}}'to='vm.to'></smart-scroll>

Let's consider parameters :

  • sens - this argument is opposite to sens itivity of scrolling, so 1 means very big sensitivity.
  • limit - size of portion, limit part of limitTo
  • height - simple corresponding html style attribute for root directive
  • length - length of array (is watched )
  • to - current position, from which portion is started (is changed), begin part of limitTo

Directive consists of two div tags :one with the height specified as parameter and with scrolling capability and second which located inside former.Scroll position of root div will have two way data binding with to parameter :the greater the scroll pulls down the more to parameter should become and vice versa.Scrolling range depends on only height of inner div, because corresponding property of root div is a constant value.Height of inner div should be proportional to the (array.length - limit) i.e.(scope.length - attrs.limit), because we should be able to show all records, also we provide very important and strictly accordance :sens * 1 pixelOfScrolling = 1 record, so to =ScrollPosition/(sens * 1 pixelOfScrolling)i.e.scope.to = scrolled/attrs.sens

So height of inner div regulates scrolling range, which is proportional to the array.length and scroll position of root div connected with to parameter i.e. what portion of records (from what index of array ) should be shown.

I created sample with all of this code :https://plnkr.co/edit/BbzmtZkLcAQu3USlO3CV. at this example i also add filter on array, so you can show not only some portion of original array, but also from filtered one with some conditions.it can be very comfortable to combine them together.

I will reproduce this example :

HTML :

<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"/><scriptsrc="https://code.jquery.com/jquery-3.1.1.min.js"integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="crossorigin="anonymous"></script><linkrel="stylesheet"href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"crossorigin="anonymous"><scriptsrc="//code.angularjs.org/snapshot/angular.min.js"></script><scriptsrc="app.js"></script></head><bodyng-app="app"><divng-controller="MyController as vm"><divclass="row"><divclass="col-sm-8"><divclass="form-group"><label>Search</label><inputclass="form-control"ng-model="vm.search"/></div></div><divclass="col-sm-2"><divclass="form-group"><label>&nbsp;</label><buttontype="button"class="btn btn-success col-sm-12"ng-click="vm.add()">Add to Top</button></div></div></div><divclass='row'><divclass="col-sm-10"><tableclass="table table-bordered"style="margin-bottom:0"><thead><tr><th>Name</th><th>Age</th><th></th></tr></thead><tbodyng-init="vm.to=0"><trng-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><ang-click="vm.removeItem(item)"href='#'>X</a></td></tr></tbody><tr><th>Total:</th><thcolspan='2'>{{vm.quantity}}</th></tr></table></div><divclass="col-sm-1"style="margin-top:0;padding-left: 0"><smart-scrollsens='10'limit='10'height='400'length='{{vm.quantity}}'to='vm.to'></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)

Conclusions

At this article i showed how to solve problem with performance degradation at case of usage of ng-repeat with very big iterated array.Solution is based on idea to show and render only visible for user portion of this array instead of whole one.this goal can be archived by using simple scrolling with AngularJS feature :limitTo and implemented as custom directive, where position of scrolling is reflected to array 's index which is a top border of showed portion(window).