如何在庞大的数据集(angular.js)上提高ngRepeat的性能?

2020/10/26 01:22 · javascript ·  · 0评论

我有一个巨大的数据集,其中包含数千行,每个行有大约10个字段,大约2MB的数据。我需要在浏览器中显示它。最简单的方法(获取数据,将其放入$scopeng-repeat=""完成工作)可以很好地工作,但是当它开始将节点插入DOM时,它会使浏览器冻结大约半分钟。我应该如何解决这个问题?

一种选择是将行逐行追加,$scope并等待ngRepeat完成向DOM中插入一个块后再移至下一个。但是AFAIK ngRepeat在完成“重复”操作时不会返回报告,因此会很丑陋。

另一种选择是将服务器上的数据拆分为页面,然后在多个请求中获取它们,但这更加丑陋。

我浏览了Angular文档以查找类似的内容ng-repeat="data in dataset" ng-repeat-steps="500",但没有找到任何东西。我对Angular的方法还很陌生,所以有可能我完全不了解这一点。最佳做法是什么?

我同意@ AndreM96的观点,最好的方法是仅显示有限数量的行,更快更好的UX,这可以通过分页或无限滚动来完成。

使用limitTo过滤器,使用Angular进行无限滚动非常简单您只需要设置初始限制,当用户要求更多数据时(为简便起见,我使用按钮)就可以增加限制。

<table>
    <tr ng-repeat="d in data | limitTo:totalDisplayed"><td>{{d}}</td></tr>
</table>
<button class="btn" ng-click="loadMore()">Load more</button>

//the controller
$scope.totalDisplayed = 20;

$scope.loadMore = function () {
  $scope.totalDisplayed += 20;  
};

$scope.data = data;

这是一个JsBin

这种方法对手机可能是个问题,因为通常在滚动大量数据时它们会滞后,因此在这种情况下,我认为分页更为合适。

为此,您将需要limitTo过滤器和自定义过滤器来定义所显示数据的起点。

这是带有分页JSBin

Ionic的collectionRepeat指令和类似的其他实现方法体现了最热且可以说是最具扩展性的方法来克服大型数据集的这些挑战一个比较好的术语是“遮挡剔除”,但您可以将其总结为:不要仅仅将呈现的DOM元素的数量限制为任意(但仍然很高)的分页数,例如50、100、500 ... ,限制为用户只能看到的尽可能多的元素

如果您执行诸如所谓的“无限滚动”之类的操作,则会在某种程度上减少初始DOM计数,但是经过几次刷新后它会很快膨胀,因为所有这些新元素都位于底部。滚动成为爬网,因为滚动全部与元素计数有关。没有什么是无限的。

collectionRepeat方法是仅使用视口中适合的尽可能多的元素,然后对其进行回收当一个元素旋转出视图时,它会与渲染树分离,重新填充列表中新项目的数据,然后重新附加到列表另一端的渲染树。这是人类已知的最快方法,它利用有限的现有元素集来获取和接收DOM中的新信息,而不是传统的创建/销毁...创建/销毁周期。使用这种方法,您可以真正实现无限滚动。

请注意,您不必使用Ionic即可使用/ hack / adaptcollectionRepeat或类似的任何其他工具。这就是为什么他们称其为开源。:-)(也就是说,Ionic团队正在做一些非常巧妙的事情,值得您注意。)


至少有一个出色的示例,可以在React中做非常类似的事情。您只是选择不对树中未显示的任何内容进行渲染,而不是对具有更新内容的元素进行回收。尽管它们非常简单的POC实施允许一些闪烁,但它可以快速处理5000个项目。


另外...track by即使在数据集较小的情况下,使用也是非常有用的,以呼应其他一些帖子认为它是强制性的。

我建议看一下:

优化AngularJS:1200ms至35ms

他们通过优化ng-repeat的4个部分制定了新的指令:

优化#1:缓存DOM元素

优化#2:聚集观察者

优化#3:延迟元素创建

优化#4:绕过监视程序的隐藏元素

该项目在github上:

用法:

1-在单页应用程序中包含以下文件:

  • core.js
  • scalyr.js
  • slyEvaluate.js
  • slyRepeat.js

2-添加模块依赖性:

var app = angular.module("app", ['sly']);

3-替换ng-repeat

<tr sly-repeat="m in rows"> .....<tr>

请享用!

除了上述所有提示(例如逐个循环和较小的循环)之外,这一提示也对我有很大帮助

<span ng-bind="::stock.name"></span>

这段代码会在名称加载后立即显示该名称,并在此之后停止监视。同样,对于ng-repeats,它可以用作

<div ng-repeat="stock in ::ctrl.stocks">{{::stock.name}}</div>

但是它仅适用于AngularJS 1.3版或更高版本。来自
http://www.befundoo.com/blog/optimizing-ng-repeat-in-angularjs/

您可以使用“跟踪依据”来提高效果:

<div ng-repeat="a in arr track by a.trackingKey">

比...快:

<div ng-repeat="a in arr">

参考:https : //www.airpair.com/angularjs/posts/angularjs-performance-large-applications

如果所有行的高度都相等,则绝对应该看一下虚拟化的ng-repeat:http : //kamilkp.github.io/angular-vs-repeat/

演示看起来很有希望(并支持惯性滚动)

虚拟滚动是处理庞大列表和大型数据集时提高滚动性能的另一种方法。

实现此目的的一种方法是使用Angular Material, md-virtual-repeat如本演示中演示的50,000项所示

直接从虚拟重复文档中获取:

虚拟重复是ng-repeat的一种有限替代,它仅呈现足够多的dom节点以填充容器并在用户滚动时对其进行回收。

规则一:切勿让用户等待任何东西。

要记住,一个需要10秒的生命增长页面的显示速度要比在黑屏之前等待3秒并立即获得全部显示要快得多。

因此,而不是化妆的页面速度快,只是让页面显得要快,即使最后的结果是慢:

function applyItemlist(items){
    var item = items.shift();
    if(item){
        $timeout(function(){
            $scope.items.push(item);
            applyItemlist(items);
        }, 0); // <-- try a little gap of 10ms
    }
}

上面的代码使列表看起来逐行增长,并且总是比一次渲染慢。但是对于用户来说,它似乎更快。

另一个版本@Steffomio

无需单独添加每个项目,我们可以按块添加项目。

// chunks function from here: 
// http://stackoverflow.com/questions/8495687/split-array-into-chunks#11764168
var chunks = chunk(folders, 100);

//immediate display of our first set of items
$scope.items = chunks[0];

var delay = 100;
angular.forEach(chunks, function(value, index) {
    delay += 100;

    // skip the first chuck
    if( index > 0 ) {
        $timeout(function() {
            Array.prototype.push.apply($scope.items,value);
        }, delay);
    }       
});

有时发生了什么,您在几毫秒内就从服务器(或后端)获取了数据(例如,我假设它是100毫秒),但是要花更多的时间才能在我们的网页上显示(假设花费900毫秒来显示)。

因此,这里发生的是800毫秒,仅用于渲染网页。

我在Web应用程序中所做的是,我使用了分页(或者也可以使用无限滚动)来显示数据列表。假设我正在显示50个数据/页。

因此,我不会一次加载渲染所有数据,最初只加载50个数据,而这只需要50毫秒(我在这里假设)。

因此这里的总时间从900毫秒减少到150毫秒,一旦用户请求下一页然后显示下50个数据,依此类推。

希望这可以帮助您提高性能。祝一切顺利

Created a directive (ng-repeat with lazy loading) 

当它到达页面底部并删除一半以前加载的数据时加载数据,当它到达div顶部时再次加载以前的数据(取决于页码)将删除当前数据的一半,所以在DOM上一次只存在有限的数据,这可能会导致更好的性能,而不是在加载时呈现整个数据。

HTML代码:

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
    <script data-require="angular.js@1.3.x" src="https://code.angularjs.org/1.3.20/angular.js" data-semver="1.3.20"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="ListController">
  <div class="row customScroll" id="customTable" datafilter pagenumber="pageNumber" data="rowData" searchdata="searchdata" itemsPerPage="{{itemsPerPage}}"  totaldata="totalData"   selectedrow="onRowSelected(row,row.index)"  style="height:300px;overflow-y: auto;padding-top: 5px">

    <!--<div class="col-md-12 col-xs-12 col-sm-12 assign-list" ng-repeat="row in CRGC.rowData track by $index | orderBy:sortField:sortReverse | filter:searchFish">-->
    <div class="col-md-12 col-xs-12 col-sm-12 pdl0 assign-list" style="padding:10px" ng-repeat="row in rowData" ng-hide="row[CRGC.columns[0].id]=='' && row[CRGC.columns[1].id]==''">
        <!--col1-->

        <div ng-click ="onRowSelected(row,row.index)"> <span>{{row["sno"]}}</span> <span>{{row["id"]}}</span> <span>{{row["name"]}}</span></div>
      <!--   <div class="border_opacity"></div> -->
    </div>

</div>

  </body>

</html>

角度代码:

var app = angular.module('plunker', []);
var x;
ListController.$inject = ['$scope', '$timeout', '$q', '$templateCache'];

function ListController($scope, $timeout, $q, $templateCache) {
  $scope.itemsPerPage = 40;
  $scope.lastPage = 0;
  $scope.maxPage = 100;
  $scope.data = [];
  $scope.pageNumber = 0;


  $scope.makeid = function() {
    var text = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    for (var i = 0; i < 5; i++)
      text += possible.charAt(Math.floor(Math.random() * possible.length));

    return text;
  }


  $scope.DataFormFunction = function() {
      var arrayObj = [];
      for (var i = 0; i < $scope.itemsPerPage*$scope.maxPage; i++) {
          arrayObj.push({
              sno: i + 1,
              id: Math.random() * 100,
              name: $scope.makeid()
          });
      }
      $scope.totalData = arrayObj;
      $scope.totalData = $scope.totalData.filter(function(a,i){ a.index = i; return true; })
      $scope.rowData = $scope.totalData.slice(0, $scope.itemsperpage);
    }
  $scope.DataFormFunction();

  $scope.onRowSelected = function(row,index){
    console.log(row,index);
  }

}

angular.module('plunker').controller('ListController', ListController).directive('datafilter', function($compile) {
  return {
    restrict: 'EAC',
    scope: {
      data: '=',
      totalData: '=totaldata',
      pageNumber: '=pagenumber',
      searchdata: '=',
      defaultinput: '=',
      selectedrow: '&',
      filterflag: '=',
      totalFilterData: '='
    },
    link: function(scope, elem, attr) {
      //scope.pageNumber = 0;
      var tempData = angular.copy(scope.totalData);
      scope.totalPageLength = Math.ceil(scope.totalData.length / +attr.itemsperpage);
      console.log(scope.totalData);
      scope.data = scope.totalData.slice(0, attr.itemsperpage);
      elem.on('scroll', function(event) {
        event.preventDefault();
      //  var scrollHeight = angular.element('#customTable').scrollTop();
      var scrollHeight = document.getElementById("customTable").scrollTop
        /*if(scope.filterflag && scope.pageNumber != 0){
        scope.data = scope.totalFilterData;
        scope.pageNumber = 0;
        angular.element('#customTable').scrollTop(0);
        }*/
        if (scrollHeight < 100) {
          if (!scope.filterflag) {
            scope.scrollUp();
          }
        }
        if (angular.element(this).scrollTop() + angular.element(this).innerHeight() >= angular.element(this)[0].scrollHeight) {
          console.log("scroll bottom reached");
          if (!scope.filterflag) {
            scope.scrollDown();
          }
        }
        scope.$apply(scope.data);

      });

      /*
       * Scroll down data append function
       */
      scope.scrollDown = function() {
          if (scope.defaultinput == undefined || scope.defaultinput == "") { //filter data append condition on scroll
            scope.totalDataCompare = scope.totalData;
          } else {
            scope.totalDataCompare = scope.totalFilterData;
          }
          scope.totalPageLength = Math.ceil(scope.totalDataCompare.length / +attr.itemsperpage);
          if (scope.pageNumber < scope.totalPageLength - 1) {
            scope.pageNumber++;
            scope.lastaddedData = scope.totalDataCompare.slice(scope.pageNumber * attr.itemsperpage, (+attr.itemsperpage) + (+scope.pageNumber * attr.itemsperpage));
            scope.data = scope.totalDataCompare.slice(scope.pageNumber * attr.itemsperpage - 0.5 * (+attr.itemsperpage), scope.pageNumber * attr.itemsperpage);
            scope.data = scope.data.concat(scope.lastaddedData);
            scope.$apply(scope.data);
            if (scope.pageNumber < scope.totalPageLength) {
              var divHeight = $('.assign-list').outerHeight();
              if (!scope.moveToPositionFlag) {
                angular.element('#customTable').scrollTop(divHeight * 0.5 * (+attr.itemsperpage));
              } else {
                scope.moveToPositionFlag = false;
              }
            }


          }
        }
        /*
         * Scroll up data append function
         */
      scope.scrollUp = function() {
          if (scope.defaultinput == undefined || scope.defaultinput == "") { //filter data append condition on scroll
            scope.totalDataCompare = scope.totalData;
          } else {
            scope.totalDataCompare = scope.totalFilterData;
          }
          scope.totalPageLength = Math.ceil(scope.totalDataCompare.length / +attr.itemsperpage);
          if (scope.pageNumber > 0) {
            this.positionData = scope.data[0];
            scope.data = scope.totalDataCompare.slice(scope.pageNumber * attr.itemsperpage - 0.5 * (+attr.itemsperpage), scope.pageNumber * attr.itemsperpage);
            var position = +attr.itemsperpage * scope.pageNumber - 1.5 * (+attr.itemsperpage);
            if (position < 0) {
              position = 0;
            }
            scope.TopAddData = scope.totalDataCompare.slice(position, (+attr.itemsperpage) + position);
            scope.pageNumber--;
            var divHeight = $('.assign-list').outerHeight();
            if (position != 0) {
              scope.data = scope.TopAddData.concat(scope.data);
              scope.$apply(scope.data);
              angular.element('#customTable').scrollTop(divHeight * 1 * (+attr.itemsperpage));
            } else {
              scope.data = scope.TopAddData;
              scope.$apply(scope.data);
              angular.element('#customTable').scrollTop(divHeight * 0.5 * (+attr.itemsperpage));
            }
          }
        }
    }
  };
});

指令演示

Another Solution: If you using UI-grid in the project then  same implementation is there in UI grid with infinite-scroll.

根据分区的高度,它会加载数据,并在滚动时添加新数据,并删除先前的数据。

HTML代码:

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <link rel="stylesheet" href="https://cdn.rawgit.com/angular-ui/bower-ui-grid/master/ui-grid.min.css" type="text/css" />
    <script data-require="angular.js@1.3.x" src="https://code.angularjs.org/1.3.20/angular.js" data-semver="1.3.20"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-grid/4.0.6/ui-grid.js"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="ListController">
     <div class="input-group" style="margin-bottom: 15px">
      <div class="input-group-btn">
        <button class='btn btn-primary' ng-click="resetList()">RESET</button>
      </div>
      <input class="form-control" ng-model="search" ng-change="abc()">
    </div>

    <div data-ui-grid="gridOptions" class="grid" ui-grid-selection  data-ui-grid-infinite-scroll style="height :400px"></div>

    <button ng-click="getProductList()">Submit</button>
  </body>

</html>

角度代码:

var app = angular.module('plunker', ['ui.grid', 'ui.grid.infiniteScroll', 'ui.grid.selection']);
var x;
angular.module('plunker').controller('ListController', ListController);
ListController.$inject = ['$scope', '$timeout', '$q', '$templateCache'];

function ListController($scope, $timeout, $q, $templateCache) {
    $scope.itemsPerPage = 200;
    $scope.lastPage = 0;
    $scope.maxPage = 5;
    $scope.data = [];

    var request = {
        "startAt": "1",
        "noOfRecords": $scope.itemsPerPage
    };
    $templateCache.put('ui-grid/selectionRowHeaderButtons',
        "<div class=\"ui-grid-selection-row-header-buttons \" ng-class=\"{'ui-grid-row-selected': row.isSelected}\" ><input style=\"margin: 0; vertical-align: middle\" type=\"checkbox\" ng-model=\"row.isSelected\" ng-click=\"row.isSelected=!row.isSelected;selectButtonClick(row, $event)\">&nbsp;</div>"
    );


    $templateCache.put('ui-grid/selectionSelectAllButtons',
        "<div class=\"ui-grid-selection-row-header-buttons \" ng-class=\"{'ui-grid-all-selected': grid.selection.selectAll}\" ng-if=\"grid.options.enableSelectAll\"><input style=\"margin: 0; vertical-align: middle\" type=\"checkbox\" ng-model=\"grid.selection.selectAll\" ng-click=\"grid.selection.selectAll=!grid.selection.selectAll;headerButtonClick($event)\"></div>"
    );

    $scope.gridOptions = {
        infiniteScrollDown: true,
        enableSorting: false,
        enableRowSelection: true,
        enableSelectAll: true,
        //enableFullRowSelection: true,
        columnDefs: [{
            field: 'sno',
            name: 'sno'
        }, {
            field: 'id',
            name: 'ID'
        }, {
            field: 'name',
            name: 'My Name'
        }],
        data: 'data',
        onRegisterApi: function(gridApi) {
            gridApi.infiniteScroll.on.needLoadMoreData($scope, $scope.loadMoreData);
            $scope.gridApi = gridApi;
        }
    };
    $scope.gridOptions.multiSelect = true;
    $scope.makeid = function() {
        var text = "";
        var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

        for (var i = 0; i < 5; i++)
            text += possible.charAt(Math.floor(Math.random() * possible.length));

        return text;
    }
    $scope.abc = function() {
        var a = $scope.search;
        x = $scope.searchData;
        $scope.data = x.filter(function(arr, y) {
            return arr.name.indexOf(a) > -1
        })
        console.log($scope.data);
        if ($scope.gridApi.grid.selection.selectAll)
            $timeout(function() {
                $scope.gridApi.selection.selectAllRows();
            }, 100);
    }


    $scope.loadMoreData = function() {
        var promise = $q.defer();
        if ($scope.lastPage < $scope.maxPage) {
            $timeout(function() {
                var arrayObj = [];
                for (var i = 0; i < $scope.itemsPerPage; i++) {
                    arrayObj.push({
                        sno: i + 1,
                        id: Math.random() * 100,
                        name: $scope.makeid()
                    });
                }

                if (!$scope.search) {
                    $scope.lastPage++;
                    $scope.data = $scope.data.concat(arrayObj);
                    $scope.gridApi.infiniteScroll.dataLoaded();
                    console.log($scope.data);
                    $scope.searchData = $scope.data;
                    // $scope.data = $scope.searchData;
                    promise.resolve();
                    if ($scope.gridApi.grid.selection.selectAll)
                        $timeout(function() {
                            $scope.gridApi.selection.selectAllRows();
                        }, 100);
                }


            }, Math.random() * 1000);
        } else {
            $scope.gridApi.infiniteScroll.dataLoaded();
            promise.resolve();
        }
        return promise.promise;
    };

    $scope.loadMoreData();

    $scope.getProductList = function() {

        if ($scope.gridApi.selection.getSelectedRows().length > 0) {
            $scope.gridOptions.data = $scope.resultSimulatedData;
            $scope.mySelectedRows = $scope.gridApi.selection.getSelectedRows(); //<--Property undefined error here
            console.log($scope.mySelectedRows);
            //alert('Selected Row: ' + $scope.mySelectedRows[0].id + ', ' + $scope.mySelectedRows[0].name + '.');
        } else {
            alert('Select a row first');
        }
    }
    $scope.getSelectedRows = function() {
        $scope.mySelectedRows = $scope.gridApi.selection.getSelectedRows();
    }
    $scope.headerButtonClick = function() {

        $scope.selectAll = $scope.grid.selection.selectAll;

    }
}

带有无限滚动演示的UI网格演示

对于大数据集和多个值下降的情况,最好使用ng-options而不是ng-repeat

ng-repeat之所以慢,是因为它会遍历所有即将到来的值,而ng-options只是显示为选择选项。

ng-options='state.StateCode as state.StateName for state in States'>

比快得多

<option ng-repeat="state in States" value="{{state.StateCode}}">
    {{state.StateName }}
</option>
本文地址:http://javascript.askforanswer.com/ruhezaipangdadeshujujiangular-jsshangtigaongrepeatdexingneng.html
文章标签: ,   ,  
版权声明:本文为原创文章,版权归 javascript 所有,欢迎分享本文,转载请保留出处!

文件下载

老薛主机终身7折优惠码boke112

上一篇:
下一篇:

评论已关闭!