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

Javascript 如何在庞大的数据集(angular.js)上提高ngRepeat的性能?,javascript,angularjs,angularjs-ng-repeat,Javascript,Angularjs,Angularjs Ng Repeat,我有一个数千行的巨大数据集,每行大约有10个字段,大约2MB的数据。我需要在浏览器中显示它。最简单的方法(获取数据,将其放入$scope,让ng repeat=”“完成其工作)工作正常,但当它开始将节点插入DOM时,浏览器会冻结大约半分钟。我应该如何处理这个问题 一个选项是以增量方式将行追加到$scope,并等待ngRepeat完成将一个块插入DOM,然后再移动到下一个块。但AFAIK ngRepeat在完成“重复”后不会报告,所以它会很难看 另一种选择是将服务器上的数据拆分为多个页面,并在多个

我有一个数千行的巨大数据集,每行大约有10个字段,大约2MB的数据。我需要在浏览器中显示它。最简单的方法(获取数据,将其放入
$scope
,让
ng repeat=”“
完成其工作)工作正常,但当它开始将节点插入DOM时,浏览器会冻结大约半分钟。我应该如何处理这个问题

一个选项是以增量方式将行追加到
$scope
,并等待
ngRepeat
完成将一个块插入DOM,然后再移动到下一个块。但AFAIK ngRepeat在完成“重复”后不会报告,所以它会很难看

另一种选择是将服务器上的数据拆分为多个页面,并在多个请求中获取它们,但这更糟糕


我在Angular文档中搜索类似于
ng repeat=“数据集中的数据”ng repeat steps=“500”
,但什么也没找到。我对棱角的方式是相当陌生的,所以我可能完全忽略了这一点。这方面的最佳实践是什么?

我同意@AndreM96的观点,即最好的方法是只显示有限数量的行,更快更好,这可以通过分页或无限滚动来实现

带角度的无限滚动带过滤器非常简单。您只需设置初始限制,当用户要求更多数据时(为了简单起见,我使用了一个按钮),您可以增加限制

<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;

{{d}
加载更多
//控制器
$scope.totalDisplayed=20;
$scope.loadMore=函数(){
$scope.totalDisplayed+=20;
};
$scope.data=数据;
这是一本书

这种方法对手机来说可能是个问题,因为它们在滚动大量数据时通常会出现延迟,所以在这种情况下,我认为分页更合适

要做到这一点,您将需要Limito过滤器和自定义过滤器来定义所显示数据的起点


这是一个带有分页的示例。

如果所有行的高度都相等,您一定要查看虚拟化重复:

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

您可以使用“跟踪方式”来提高性能:

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

快于:

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


参考:

我建议您看看:

优化角度js:1200ms至35ms

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

优化#1:缓存DOM元素

优化#2:聚合观察者

优化#3:延迟元素创建

优化#4:绕过隐藏元素的观察程序

该项目位于github上:

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

  • core.js
  • scalyr.js
  • slyeevaluate.js
  • slyRepeat.js
2-添加模块依赖项:

var app = angular.module("app", ['sly']);
3-更换ng重复

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

享受吧

用大型数据集克服这些挑战的最热门、也可以说是最具可扩展性的方法体现在和其他类似的实现中。这是一个奇特的术语,但您可以将其总结为:不要仅将渲染DOM元素的计数限制为任意(但仍然很高)分页数,如50、100、500。。。相反,只限制用户可以看到的元素数量

如果你做了一些通常被称为“无限滚动”的事情,你会在某种程度上减少初始DOM计数,但在几次刷新后它会迅速膨胀,因为所有这些新元素都是在底部加上的。滚动是一种爬行,因为滚动是关于元素计数的。没有什么是无限的

然而,
collectionRepeat
方法是只使用视口中适合的元素,然后回收它们。当一个元素旋转出视图时,它将与渲染树分离,重新填充列表中新项目的数据,然后重新附着到列表另一端的渲染树。这是人类已知的利用有限的现有元素,而不是传统的创建/销毁循环,从DOM中获取新信息的最快方法。。。创建/销毁。使用这种方法,您可以真正实现无限滚动

请注意,使用/hack/adapt
collectionRepeat
或任何其他类似工具时,您不必使用Ionic。这就是为什么他们称之为开源的原因。:-)(也就是说,爱奥尼亚团队正在做一些非常有创意的事情,值得你关注。)


在React中,至少有一种做法非常类似。只需选择不渲染树中不在视图中的任何内容,而不是使用更新的内容循环使用元素。它在5000件物品上运行得非常快,尽管它们非常简单的POC实现允许一些闪烁



还有。。。为了回应其他一些帖子,使用
track by
非常有用,即使数据集较小。考虑到它是强制性的。

< P>除了所有这些提示,如RouthBy和更小的循环,这一个也帮助了我很多

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

这段代码将在加载后打印名称,然后停止查看。类似地,对于ng重复,它可以用作

<div ng-repeat="stock in ::ctrl.stocks">{{::stock.name}}</div>
{{::stock.name}
但是,它仅适用于AngularJS 1.3版及更高版本。 从…起

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

实现这一点的一种方法是使用
md virtual repeat
,如本文所示

直接取自虚拟重复的文档:

Virtual repeat是ng repeat的有限替代品,它只呈现足够的dom节点来填充容器,并在用户滚动时循环使用它们


规则1:永远不要让用户
function applyItemlist(items){
    var item = items.shift();
    if(item){
        $timeout(function(){
            $scope.items.push(item);
            applyItemlist(items);
        }, 0); // <-- try a little gap of 10ms
    }
}
ng-options='state.StateCode as state.StateName for state in States'>
<option ng-repeat="state in States" value="{{state.StateCode}}">
    {{state.StateName }}
</option>
// 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);
    }       
});
Created a directive (ng-repeat with lazy loading) 
<!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.
<!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;

    }
}