站点工具

用户工具


# 如何一次性加载10万条数据(虚拟长列表)

## 前言

如何一次性加载并渲染10万条数据,渲染无延时、页面还不卡顿?这是面试官非常爱问的一道问题。最早听到这个问题的时候,我和很多人一样嗤之以鼻——这要么是面试官闲的XX,要么是后端太懒懒的做分页,哪有场景会一次性加载10万条数据,难道不会用分页或者懒加载?后来兴趣爱好玩股票期货,看到价格分时数据平均1秒2条,一天恰好十几万条。这十几万条数据要实现数据可视化真得一次性拿到,既然数据都已经到了也没必要用分页或者懒加载了。要做的就是如何保证用户滚动十几万条数据的时候既流畅,体验又好。

## 一张图看懂虚拟长列原理

![](https://pic3.zhimg.com/v2-8048dbc387df694c2a83f50424c4c986_b.jpg)

图1展示了未使用虚拟长列表的原始状态。假设列表原始长度为15(也可能数十万),绿色区域为包裹列表的容器,紫色窗口为可视区域。不管列表实际有多少,用户能看到的只是紫色可视窗口透出的区域。

图2~图5展示了使用虚拟长列后用户滚动的过程和效果。

在图2里,我们_设置列表容器的高度_(绿色背景)使其和图1(未使用虚拟列表)保持一致,但实际存放的DOM节点只有紫色窗口能容纳的个数\(图中是4个\)。透过可视窗口,用户看到的效果和图1是一致的,且滚动条的位置也和图1一致。

当用户滚动一小段距离(如图3所示)后,第1行和第2行的一半被遮盖,第5行之后后的空白漏出。此时,可_获取滚动的距离__根据滚动距离和每行的高度计算出被滚出可视区域的行数_。如图4所示, 我们需要_删除被滚出可视区的DOM_(第1行),同时_新增需要展现的DOM_(第6行)。在删除隐藏的DOM后_设置绿色容器的上padding_,来填充删除DOM留下的空缺\(图5灰色部分为新增的paddingTop\),来保证透过可是区域的DOM节点视觉上没有发生位置变动。

通过图3、4、5的操作,用户实际的视觉效果\(紫色框内部分\),和操作体验和不使用虚拟长列表的图1基本一致,但本质上存在的真实DOM节点只有紫色窗口内的寥寥数个。

如果使用了Vue或者React,_删除隐藏的DOM_,同时_新增需要展现的DOM_ 的操作只需要设置数据即可\(如:\[1,2,3,4,5\]换成\[2,3,4,5,6\]\)。

## Vue3 实现虚拟长列表

```html <div id=“app”></div>

<script src=“https://unpkg.com/vue@next”>

<script> let t = Date.now() const { ref, reactive, computed, onMounted, createApp } = Vue; createApp({

template: `  
  <div class="container" @scroll="onScroll">
    <div class="panel" ref="panel"
         :style="{paddingTop: paddingTop + 'px'}">
      <div class="item" v-for="item in visibleList" :key="item">
        {{ item }}
      </div>
    </div>
  </div>`,

setup() {
  let panel = ref(null)  //列表容器DOM
  
  //构造的长列表原始数据
  let raw = Array(100000).fill(0).map((v, i) => `item-${i}`); 
  let count = 10;      //实际渲染DOM的列表数量
  let start = ref(0);  //从长列表数组总截取数据的起点 
  let end = ref(10);   //从长列表数组总截取数据的终点
  let itemHeight = 30; //单个列表项的高度
  let paddingTop = ref(0); //列表容器的上内边距
  let totalHeight = raw.length*itemHeight  //原始数据理论上完全渲染后占据的总高度

  let visibleList = computed(() => raw.slice(start.value, end.value)); //根据起点和终点获取要渲染的数据
  onMounted(() => panel.value.style.height = totalHeight + 'px') //在mounted后设置列表容器的高度
  
  //滚动-->根据滚动距离计算起点和终点的下标-->计算属性得到visibleList-->真实DOM被替换 同时设置paddingTop让元素视觉上没跳动
  const onScroll = function (e) {
    start.value = Math.floor(e.target.scrollTop / itemHeight); //当滚动后,重新计算起点的位置
    end.value = start.value + count; //设置终点的位置
    paddingTop.value = start.value*itemHeight; 
  };

  return {
    visibleList, paddingTop, panel, onScroll
  };
}

}).mount('#app');

</script>

<style>
  * {
    box-sizing: border-box;
    margin: 0;
  }
  .container {
    height: 300px;
    overflow-y: scroll;
  }
 
  .item {
    border: 1px solid #eee;
    line-height: 30px;
    height: 30px;
    padding: 0 10px;
    cursor: pointer;
  }
 
</style>
预览效果: [http://js.jirengu.com/hepud](https://link.zhihu.com/?target=http%3A//js.jirengu.com/hepud)

## 优化

以上短短几行核心代码实现了虚拟长列表,不过存在以下问题

1.  可视区域⾼度、展⽰的列表数量、⼦项的⾼度都是固定的,⽤起来不便
2.  滚动时触发渲染的频率太⾼,导致滚动时没有极致顺滑\(移动端更明显\)
3.  滚动过快时容易出现短暂⽩屏,

可以针对原始原始DOMO做进一步优化

1.  可视区域高度不做限制,列表子项高度不固定,展示列表的数量可根据可视区域高度自动计算
2.  对滚动做使用防抖或者节流处理,减少计算频率
3.  第2条可提升性能,但会延长滚动时上下方出现空白的时间。可通过加列表Buff来处理\(如可视区域展示10个列表,再其上方隐藏10个列表,其下方隐藏10个列表\)。

具体实现,可参考我的Vue3课程该免费章节的讲解 [Vue3实现虚拟长列表\(饥人谷\)](https://link.zhihu.com/?target=https%3A//xiedaimala.com/tasks/42df6e5e-0834-400e-8873-9fa2dff2f888) 。或者直接看优化后的代码: [thirsty-moon-il9kj \- CodeSandbox](https://link.zhihu.com/?target=https%3A//codesandbox.io/s/thirsty-moon-il9kj%3Ffile%3D/src/App.vue)

如要了解课程,加微信 xiedaimala02

> 饥人谷一直致力于培养有灵魂的编程者,打造专业有爱的国内前端技术圈子。如造梦师一般帮助近千名不甘寂寞的追梦人把编程梦变为现实,他们以饥人谷为起点,足迹遍布包括facebook、阿里巴巴、百度、网易、京东、今日头条、大众美团、饿了么、ofo在内的国内外大小企业。 了解培训课程:加微信 [xiedaimala03](https://wiki.jirengu.com/lib/exe/fetch.php?w=400&tok=5c45ca&media=%E9%A5%A5%E4%BA%BA%E8%B0%B7%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E5%B0%8F%E5%8A%A9%E7%90%86black.png),官网:https://jirengu.com
若愚 · 2023/02/08 19:01 · 如何一次性加载10万条数据_虚拟长列表.1675854089.txt.gz