前端列表虚拟化

VueJavaScript虚拟化

2023年02月08日 11:49:461330

为什么需要虚拟化列表?

先来看看,正常情况下一次性渲染10万行数据,浏览器需要消耗的时间:

//cdn.shiniest.cn/static/202302/rendering_time.png

渲染阶段耗时超过了2秒,这还只是最简单的文字渲染,在真实业务中往往会更加复杂,如果不进行优化,这将是无法接受的。

什么是虚拟化?

我也是偶然接触到这个概念的,第一次接触是在 vscode.dev ,闲来无事我按了F12,想膜拜一下大佬风范,结果就发现了神奇的一幕:编辑器内容并不是全部渲染的,而是根据我滚动的位置,显示特定位置的内容,就好像编辑器右边有一个进度展示,其中高亮的区域就是编辑器当前展示的区域,也是编辑器渲染的内容,对于超出部分不进行渲染,难怪vscode可以秒开大文件还不卡,我想很大一部分原因在这里了。第二次是在看 Element Plus 的文档的时候,注意到多出了一个虚拟化select组件和虚拟化table组件。

由此可见,虚拟化其实就是按需渲染的一种实现手段,只对可见区域及缓冲区域的数据进行渲染,对不可见区域的数据不进行渲染。

//cdn.shiniest.cn/static/202302/vscode_code_area.png

代码实现

通过以上对虚拟化的认识,我们只需要渲染可见区域内的数据,所以将html设计为如下结构:

<div class="container">
  <div class="block"></div>
  <ul class="render">
    <li>item 1 ...</li>
    <li>item 2 ...</li>
    <li>item 3 ...</li>
    <li>...</li>
    <li>item n ...</li>
  </ul>
</div>
  • container 为列表容器,用于包裹整个列表。

  • block仅用于高度填充,使滚动条能够正常工作。

  • render 用于包裹当前渲染的数据。

实现方案为

  • 计算列表总高度

    • item的高度 * item的数量

  • 计算当前的起始index和结束index并截取相应的数据进行渲染。

    • 滚动的距离 / item的高度 = 开始的index

    • 可视区域的高度 / item的高度 = 可视的item数量

    • 开始的index + 可视的item数量 = 结束的index

  • 计算渲染容器的offset,防止跳跃闪烁

    • 滚动的距离 - (滚动的距离 % item的高度) 取余是为了防止闪烁

  • 监听容器的滚动事件,滚动发生时重新计算

//cdn.shiniest.cn/static/202302/infinite_scroll.gif

代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Virtualization List</title>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      .container { width: 400px; margin: auto; margin-top: 100px; overflow: auto; position: relative; border: 1px solid gray; }
      .container >.block { position: absolute; z-index: -1; top: 0; left: 0; width: 100%; }
      .container >ul { position: absolute; top: 0; left: 0; width: 100%; }
      .container >ul >li { background: #fff; border: 1px solid #eee; }
    </style>
  </head>
  <!-- 解决闪烁的问题 -->
  <body style="visibility: hidden;">
    <div id="app">
      <div class="container" :style="{height: `${renderHeight}px`}" @scroll="scrollEvent($event)">
        <div class="block" :style="{height: `${listHeight}px`}"></div>
        <ul class="render" :style="{transform: `translateY(${offsetDistance}px)`}">
          <li v-for="content in visibleData">{{content}}</li>
        </ul>
      </div>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script>
      const { createApp, ref, onMounted, shallowRef, reactive, computed } = Vue;
      createApp({
        setup(){
          //获取虚拟文字段落 ↓
          function getMockData(index){
            let count = 10;
            return 'index: ' + index + ' **** '.repeat(count);
          }
          //列表数据 ↓
          const listData = ref(Array.from({length: 100000}).map((item, index) => getMockData(index)));
          //单项高度 ↓
          const ceilHeight = ref(44);
          //可视区域高度 ↓
          const renderHeight = ref(500);
          //起始index ↓
          const startIndex = ref(0);
          //结束index ↓
          const endIndex = ref(0);
          //偏移量 ↓
          const offsetDistance = ref(0);
          //列表总高度 ↓
          const listHeight = computed(() => listData.value.length * ceilHeight.value);
          //可视区item数量 ↓ 向上取整
          const visibleCount = computed(() => Math.ceil(renderHeight.value / ceilHeight.value));
          //当前可显示的列表数据
          const visibleData = computed(() => listData.value.slice(startIndex.value, Math.min(endIndex.value, listData.value.length)));

          onMounted(() => {
            //解决闪烁的问题
            document.body.style.visibility = 'visible';
            startIndex.value = 0;
            endIndex.value = startIndex.value + visibleCount.value;
          })

          //滚动事件相关 ↓
          function scrollEvent(e) {
            //当前滚动的距离 ↓
            const scrollTop = e.target.scrollTop;
            //计算开始index ↓
            startIndex.value = Math.floor(scrollTop / ceilHeight.value);
            //计算结束index ↓
            endIndex.value = startIndex.value + visibleCount.value;
            //计算偏移量(防止闪烁跳跃) ↓
            offsetDistance.value = scrollTop - (scrollTop % ceilHeight.value);
          }
          return {
            visibleData,
            getMockData,
            renderHeight,
            listHeight,
            offsetDistance,
            scrollEvent
          }
        }
      }).mount('#app')
    </script>
  </body>
</html>
赞 10
收藏
分享

本作品系 原创,作者:你不熟悉的x先生

原文链接:https://shiniest.cn/blog/article/158

文本版权:文本版权归作者所有

转载需著名并注明出处(禁止商业使用)

评论和回复

0/500
    没有更多啦~
    怎么一条数据都没有呢?
简介
虚拟化其实就是按需渲染的一种实现手段,只对可见区域及缓冲区域的数据进行渲染,对不可见区域的数据不进行渲染。
目录
推荐阅读

D&D By x先生 With