虚拟列表

1,621 阅读4分钟

前言

在浏览器渲染页面的时候,当DOM节点的数量越多,每一次重绘的时候,对性能的影响也就越大。
假如我们需要展示一个信息量很大,大约有数十万条数据。我们首先会想到使用分页去处理,但是如果需求是不希望分页去查看数据,而是想通过滚动就可以看到新的内容,但是这个就会遇到一个问题,不停的加载数据,导致页面堆积的节点越来越多,所消耗的内存不断增大,最后连滚动都会卡顿。
我们分析一下,就会发现其实有很多数据我们大多数情况下是不需要看见的,如果只考虑我们能看到数据的话,其实需要渲染的数据量就会非常的少了,很好的提高了渲染的效率,减少因为大量的重绘造成不必要的影响。
经过梳理,发现虚拟列表可解决长列表导致的性能问题。

虚拟列表

虚拟列表是按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。

滚动容器元素:一般情况下,滚动容器元素是 window 对象。然而,我们可以通过布局的方式,在某个页面中任意指定一个或者多个滚动容器元素。只要某个元素能在内部产生横向或者纵向的滚动,那这个元素就是滚动容器元素考虑每个列表项只是渲染一些纯文本。在本文中,只讨论元素的纵向滚动,是一个div;
可滚动区域:滚动容器元素的内部内容区域。假设有 10000 条数据,每个列表项的高度是 50,那么可滚动的区域的高度就是 10000 * 50。可滚动区域当前的具体高度值一般可以通过(滚动容器)元素的 scrollHeight 属性获取。用户可以通过滚动来改变列表在可视区域的显示部分。
可视区域:滚动容器元素的视觉可见区域。如果容器元素是 window 对象,可视区域就是浏览器的视口大小(即视觉视口);如果容器元素是某个 div 元素,其高度是 500px,右侧有纵向滚动条可以滚动,那么视觉可见的区域500px就是可视区域。
实现虚拟列表就是在处理用户滚动时,要改变列表在可视区域的渲染部分,其具体步骤如下:

计算当前可见区域起始数据的 startIndex;
计算当前可见区域结束数据的 endIndex;
计算当前可见区域的数据,并渲染到页面中;
计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上;
计算 endIndex 对应的数据相对于可滚动区域最底部的偏移位置 endOffset,并设置到列表上; 建议参考下图理解一下上面的步骤:

应用场景

渲染10000条数据,不做虚拟列表处理,首屏的时间需要336ms,

利用虚拟列表的处理后,首屏的时间不到100ms,

代码实现

<template>
  <div id="virtualList" class="virtualList" @scroll="handleScroll">      
    <div id="content" class="list-view-content" v-bind:style="{paddingTop:startOffset + 'px',paddingBottom:endOffset+'px'}">
        <div class="list-view-item" v-for="(item,index) in visibleList" :key="index">
          <div class="col">{{item.name}}</div>
          <div class="col">{{item.value}}</div>
        </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'virtualList',

  data(){
    return {
      list:[],
      visibleList:[],
      timer:null,
      startTime:null,
      startIndex:0,
      endIndex:0,
      startOffset:0,
      endOffset:0,
      listLength:0,
    }
  },
  mounted(){
    this.startTime = new Date().getTime();
    for(let i = 0;i<10000;i++){
      let obj = {};
      obj['name'] = i;
      obj['value'] = 'test'+Math.random();      
      this.list.push(obj);
    }
    this.visibleCount = Math.ceil(1000/50);
    this.endIndex = this.startOffset + this.visibleCount;
    this.visibleList = this.list.slice(this.startOffset,this.endIndex);
    this.endOffset = (this.visibleList.length-this.endIndex)*50;
    this.listLength = this.list.length;
  },
  updated: function () {
    let lastTime = new Date().getTime();
    console.log(lastTime - this.startTime);
  },
  methods:{
    handleScroll: function(){
      if(this.timer){
        clearTimeout(this.timer);        
      }
      var vm = this;
      this.timer = setTimeout(()=>{
        var contentDiv = document.getElementById('virtualList');
        var scrollTop = contentDiv.scrollTop;
        vm.startOffset = scrollTop;
        vm.startIndex =  Math.ceil(scrollTop/50);
        vm.endIndex = vm.startIndex + vm.visibleCount;   
        vm.visibleList =  vm.list.slice(vm.startIndex,vm.endIndex);
      },400);
    }
  }  
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
.virtualList{
  height:200px;
  overflow-y:scroll;
}     

.list-view-content{
  min-height:10px;
  width:100%;  
  height:300000px;
}
.list-view-item{
  display:inlien-block;
}
.col{
  display:inline-block;
  margin-right:200px;
}
</style>