虚拟滚动的实现(适合大量的列表数据)

Posted by Yinode on Monday, April 8, 2019

TOC

假设我们拥有10w条数据,需要在Web上进行展现,但是如果我们进行实际的渲染就会发现,整个初次渲染的成本的非常之高,用户的体验是非常差的。所以诞生了一种虚拟滚动条的概念,本质上就是进行按需渲染,我们永远只渲染当前所展现的数据,一个滚动区域的视口是固定的,比如400px,我们可以计算出整个滚动区域的具体需要展现那一部分的数据,进行按需渲染。

以下是我实现了一个虚拟滚动的DEMO后的DOM结构,我事实上渲染了10w条数据,但在同一时间内,永远只渲染20条的数据(因为视口=400px,单个item高度等于20px),如此一来,我们就能减少初次渲染时的负担。

Code

<template>
  <div id="app">
    <div class="scroll-box" @scroll="onScrollHandle">
      <ul class="wrap" :style="`height:${wrapHeight}px`">
        <li
          class="item"
          v-for="v of renderList"
          :key="v.id"
          :style="`color:${v.color};top:${20 * v.id}px`"
        >{{v.id}}</li>
      </ul>
    </div>
  </div>
</template>
<script>
const viewportHeight = 400;
const viewportItemHeight = 20;
const viewportItemCount = 100000;
// 为了避免肉眼可见的消失  + 1
const renderCount = viewportHeight / viewportItemHeight + 1;

export default {
  data() {
    const list = Array(viewportItemCount)
      .fill(1)
      .map((v, i) => {
        const color = "#" + Math.floor(Math.random() * 256 ** 3).toString(16);
        const id = i;
        return {
          color,
          id
        };
      });
    return {
      list,
      renderList: [],
      wrapHeight: 0
    };
  },
  created() {
    this.wrapHeight = viewportItemHeight * viewportItemCount;
    this.renderList = this.calcluteRenderList(0);
  },
  methods: {
    calcluteRenderList(scrollTop) {
      const renderTopCount = Math.floor(scrollTop / viewportItemHeight);
      return this.list.slice(renderTopCount, renderTopCount + renderCount);
    },
    onScrollHandle(e) {
      this.renderList = this.calcluteRenderList(e.target.scrollTop);
    }
  }
};
</script>
<style lang="stylus">
html, body
  padding 0
  margin 0
.scroll-box
  overflow-y scroll
  height 400px
  .wrap
    overflow hidden
    padding 0
    margin 0
    position relative
    .item
      height 20px
      position absolute
      list-style none
</style>

事实上这个代码是非常简单的,首先我们建立了两个列表,一个是完整的数据,一个是真实用来渲染的列表,并在滚动事件触发的时候,不断去计算当前到底需要哪些列表项来展示。这里需要注意的有两点

  1. 单个Item的高度必须固定,否则无法进行计算
  2. wrap区域需要进行一次初始化,值为item的高度和

总结

当然这种长列表的需求一般来说可以用分页去解决,不要为了用而用,因为长列表滚动查找起来对于用户并不友好。在某些特殊情况下还是可以的。