Javascript 提高大型工作台上的iScroll性能

Javascript 提高大型工作台上的iScroll性能,javascript,jquery,iscroll,Javascript,Jquery,Iscroll,我正在根据用户的滚动方式以编程方式更新表格标题及其第一列位置,以保持它们对齐 我遇到的问题是,一旦我的数据集变得足够大,滚动就会变得越来越不平稳 相关代码位于最底层: iScroll.on('scroll', function(){ var pos = $('#scroller').position(); $('#pos').text('pos.left=' + pos.left + ' pos.top=' + pos.top); // code to hold fir

我正在根据用户的滚动方式以编程方式更新表格标题及其第一列位置,以保持它们对齐

我遇到的问题是,一旦我的数据集变得足够大,滚动就会变得越来越不平稳

相关代码位于最底层:

iScroll.on('scroll', function(){
    var pos = $('#scroller').position();
    $('#pos').text('pos.left=' + pos.left + ' pos.top=' + pos.top);

    // code to hold first row and first column
    $('#scroller th:nth-child(1)').css({top: (-pos.top), left: (-pos.left), position:'relative'});
    $('#scroller th:nth-child(n+1)').css({top: (-pos.top), position:'relative'});

    // this seems to be the most expensive operation:
    $('#scroller td:nth-child(1)').css({left: (-pos.left), position:'relative'});
});
我知道,通过缓存元素等方式,可以更高效地编写这篇文章。例如,我尝试将元素保存到数组中,并以更“普通”的方式更新它们的位置:

不管我回电话的速度有多快,我还是对结果不满意。你有什么建议吗


这不是你想要的答案,但这是我的2美分

Javascript动画(特别是考虑到DOM必须渲染的数量)永远不会像您希望的那样平滑。即使你能在你的机器上顺利运行,在其他人(旧电脑、浏览器等)上也会有很大的不同

如果我自己解决这个问题,我会看到两种选择

  • 去老学校,添加一个水平和垂直的滚动条。我知道这不是一个很好的解决方案,但它会很好地工作

  • 只渲染一定数量的行,并在屏幕外丢弃这些行。这可能有点复杂,但本质上您将渲染10行。一旦用户滚动到第11个应该在那里的点,渲染该点并删除第一个。你可以根据需要把它们放进放出


  • 对于实际的JS(您提到将元素放入数组),这并没有帮助。实际的起伏是由于浏览器首先需要呈现那么多的元素。

    要能够处理大量数据,您需要数据虚拟化。不过,它有一些限制

    首先,需要确定视图端口的大小。假设您希望在一行中呈现10项,在一列中呈现20项。那么它将是10x20项。在你的小提琴它的id包装的div

    然后你需要知道你拥有的数据总量。从你的小提琴它将是100x100项目。此外,您还需要知道项目(单元格)的高度和宽度。让我们以40x120(像素)为例

    因此,div#wrapper是一个视图端口,它应该具有固定大小,如10x20项。然后您需要为表格设置正确的宽度和高度。表格的高度等于列中的数据总量,包括按项目高度划分的标题。表格的宽度是按项目宽度计算的单行项目总数

    一旦设置了这些选项,div#wrapper将收到水平和垂直滚动条。现在您可以向左和向下滚动,但它只是一个空白。但是,这个空白空间能够容纳您所拥有的确切数量的数据

    然后,您需要将滚动数据向左和向上(位置),以像素为单位,并将其规格化为项目数量,这样您就可以不知道滚动了多少像素,而是知道滚动了多少项目(如果从上到下滚动,则为行)

    它可以通过划分项目高度上滚动的像素来完成。例如,您向左滚动了80px,即2项。这意味着这些项目应该是不可见的,因为你已经滚动过去了。所以您知道您滚动了2个项目,并且您知道您应该在一行中看到10个项目。这意味着您需要使用数据数组,该数组包含包含100项的行的数据,并按如下方式对其进行切片:

    var visibleItems = rowData.slice(itemsScrolled, itemsScrolled + 10);
    
    它将为您提供在当前滚动位置应在视口中可见的项目。一旦有了这些项,就需要构造html并将其附加到表中

    此外,在每个滚动事件中,您需要为t正文和thead设置顶部和左侧位置,以便它们随滚动一起移动,否则您将拥有数据,但数据将位于视口内的(0;0)

    不管怎么说,代码能说千言万语,所以这里有一把小提琴:

    注意,此方法要求高度和宽度精确,否则将无法正常工作。此外,如果您有不同尺寸的物品,也应考虑到这一点,因此,如果您有固定和相等尺寸的物品,则效果更佳。在JSFIDLE中,我注释掉了强制第一列保持不变的代码,但您可以单独呈现它

    坚持使用评论中建议的库是一个很好的解决方案,因为它可以为您处理很多案例

    如果使用react.js或vue.js,可以使渲染速度更快,只需使用!它可以处理数十万行,并且正是为此目的而构建的

    你问,它是如何工作的

    主要思想是不要用所有使用过的标记污染DOM。相反,它将列表拆分为多个簇,然后显示当前滚动位置的元素,并在列表的顶部和底部添加额外的行,以模拟表格的完整高度,这样浏览器就可以像显示完整列表一样显示滚动条


    对!!下面是对jsfiddle代码的一些改进。您可以在以下位置查看我的编辑:

    建议的改善措施包括:

  • 将JS层中的
    position:relative
    值切换到CSS层中的
    position:fixed
  • 缩短jquerydom链,这样代码就不会从根元素开始&每次$lookup都会遍历DOM。滚动条现在是根元素。一切都使用该元素的
    .find()
    ,这会创建更短的树&jQuery可以更快地遍历这些分支
  • 将日志代码从DOM中移出&移到console.log中。我添加了一个调试开关来禁用它,因为您正在查找表上最快的滚动。如果它运行得足够快,那么您可以始终重新启用它,以便在JSFIDLE中查看它。如果您真的需要在iPhone上看到这一点,那么可以将其添加到DOM中。虽然,可能没有必要去看t
    var visibleItems = rowData.slice(itemsScrolled, itemsScrolled + 10);
    
    <body>
    <div id="wrapper">
      <table id="scroller">
        <thead>
        </thead>
        <tbody>
        </tbody>
      </table>
     </div>
    </body>
    
    /* ... only the relevant bits ... */
    
    thead th {
      background-color: #99a;
      min-width: 120px;
      height: 32px;
      border: 1px solid #222;
      position: fixed; /* New */
      z-index: 9;
    }
    
    thead th:nth-child(1) {/*first cell in the header*/
      border-left: 1px solid #222; /* New: Border fix */
      border-right: 2px solid #222; /* New: Border fix */
      position: fixed; /* New */
      display: block; /*seperates the first cell in the header from the header*/
      background-color: #88b;
      z-index: 10;
    }
    
    // main code
    let debug = false;
    
    $(function(){ 
      let scroller = $('#scroller');
      let top = $('<tr/>');
      for (var i = 0; i < 100; i++) {
        let left = (i === 0) ? 0 : 1;
        top.append('<th style="left:' + ((123*i)+left) + 'px;">'+ Math.random().toString(36).substring(7) +'</th>');
      }
      scroller.find('thead').append(top);
      for (let i = 0; i < 100; i++) {
        let row = $('<tr/>');
        for (let j = 0; j < 100; j++) {
          row.append('<td>'+ Math.random().toString(36).substring(7) +'</td>');
        }
        scroller.find('tbody').append(row);
      }
    
      if (debug) console.log('initialize iscroll');
      let iScroll = null;
      try {
        iScroll = new IScroll('#wrapper', { 
          interactiveScrollbars: true, 
          scrollbars: true, 
          scrollX: true, 
          probeType: 3, 
          useTransition:false, 
          bounce:false
        });
      } catch(e) {
        if (debug) console.error(e.name + ":" + e.message + "\n" + e.stack);
      }
    
      if (debug) console.log('initialized');
    
      iScroll.on('scroll', function(){
        let pos = scroller.position();
        if (debug) console.log('pos.left=' + pos.left + ' pos.top=' + pos.top);
    
        // code to hold first row and first column
        scroller.find('th').css({top:-pos.top}); // Top Row
        scroller.find('th:nth-child(1)').css({left:-pos.left}); // Corner
        scroller.find('td:nth-child(1)').css({left:-pos.left}); // 1st Left Column
      });
    });
    
    // Position of the `#scroller` element
    // (I used two globals that may pollute the global namespace
    // that piece of code is just for explanation purpose)    
    
    var oldPosition, 
        newPosition;
    
    
    // Use transition to perform animations
    // You may set this in the stylesheet
    
    $('th').css( { 'transition': 'left 0.5s, top 0.5s' } );
    $('td').css( { 'transition': 'left 0.5s, top 0.5s' } );
    
    
    // Save the initial position
    
    newPosition = $('#scroller').position();
    oldPosition = $('#scroller').position();
    
    
    // Upon scroll just set the position value
    
    iScroll.on('scroll', function() {
        newPosition = $('#scroller').position();
    } );
    
    
    // Start the scroll worker
    
    ScrollWorker();
    
    
    function ScrollWorker() { 
    
        // Adjust the layout if position changed (your original code)
    
        if( newPosition.left != oldPosition.left || newPosition.top != oldPosition.top ) {
            $('#scroller th:nth-child(1)').css({top: (-newPosition.top), left: (-newPosition.left), position:'relative'});
            $('#scroller th:nth-child(n+1)').css({top: (-newPosition.top), position:'relative'});
            $('#scroller td:nth-child(1)').css({left: (-newPosition.left), position:'relative'});
    
            // Update the stored position
    
            oldPosition.left = newPosition.left;
            oldPosition.top  = newPosition.top;
    
            // Let animation complete then check again
            // You may adjust the timer value
            // The timer value must be higher or equal the transition time
    
            setTimeout( ScrollWorker, 500 );
    
        } else {
    
            // No changes
            // Check again after just 0.1secs
    
            setTimeout( ScrollWorker, 100 );
        }
    }
    
      // stores the scrolling operation for a tiny delay to prevent redundancy
      var fresh;
    
      // stores time of last scrolling refresh
      var lastfresh = new Date();
    
      // clears redundant scrolling operations before they are applied
      if (fresh) clearTimeout(fresh);
    
      var x = function() {
        // stores new time of scrolling refresh
        lastfresh = new Date();
        // perform scrolling update operations here...
      };
    
      // refresh instantly if it is more than 50ms out of date
      if (new Date() - lastfresh > 50) x();
    
      // otherwise, pause for half of that time to avoid wasted runs
      else fresh = setTimeout(x, 25);
    
     fresh = setTimeout(x, 25);