Vue3实现虚拟列表

4,088 阅读3分钟

当要展示的数据量很大的时候,可以采用分页的方式,但是用户在浏览信息时并不连贯。此时可以将数据缓存在本地,采用虚拟列表的方式展示,这里使用Vue3开发一个简单的虚拟列表。

思路

先说一下思路吧,这不是唯一的实现方式,也不是最好的实现方式,但是原理大同小异,这种方法相对容易理解一些。

页面结构

<div id="app">
  <div class="container">
    <div class="empty"></div>
    <ul class="list">
      <li class="item"></li>
    </ul>
  </div>
</div>

1.jpg

  1. 初始化容器:给一个容器 container 设置高度,列表将展示在该容器中,设置溢出滚动。
  2. 撑开容器:设置一个固定行高,就可以根据列表的 length 计算出该列表的实际高度,设置给一个 div 此处称为 empty ,放在容器中,这样容器已经可以开始滚动了,只不过页面中没有数据。
  3. 渲染列表:根据固定行高计算出容器可以展示几个 item ,页面始终展示该固定个数,给 container 设置 display: flex; ,让 listempty 并排展示,由于 empty 只有高度没有宽度,所以对于用户来说是不可见的,只起到撑开 container 的作用。

2.jpg

  1. 容器滚动:当滚动 container 时,根据 scrollTop 计算出有多少个 item 被滚走了,设置为 slicestart ,重新计算应该截取展示的那一部分,并给 list 设置和 scrollTop 相同的 translateY ,使列表的顶部始终和容器的顶部对齐。

代码实现

const oriData = Array.from({ length: 1000 }, (v, k) => k)
const itemHeight = 20
const emptyHeight = itemHeight * oriData.length
const containerHeight = window.innerHeight
const itemCount = Math.ceil(containerHeight / itemHeight)
  • oriData 生成一个长度为1000的数组,当作假数据
  • itemHeight 设置行高为20,即每条数据占用高度
  • emptyHeight 列表实际应该占用的高度
  • containerHeight 容器高度
  • itemCount 页面中每次展示的数量,这里用 ceil 是因为假设最后一行不够展示一个 item 了也要展示出来,否则页面最下面会空一块
const container = ref(null)
const start = ref(0)
const translateY = ref(0)
const listData = computed(() => {
  return oriData.slice(start.value, start.value + itemCount + 1)
})
  • translateY 随着页面的滚动, list 也要跟着动,否则就会从屏幕中消失
  • listData 即页面中始终展示的数据,这里设置成计算属性的原因是,列表中的数据会随着屏幕的滚动不断变化
onMounted(() => {
  container.value.addEventListener('scroll', e => {
    const { scrollTop } = e.target
    start.value = Math.floor(scrollTop / itemHeight)
    translateY.value = scrollTop + 'px'
  })
})

当容器滚动时,获取 scrollTop ,赋给 translateY ,让 list 的顶部始终和 container 顶部对其。并计算出滚动的距离相当于多少个 item ,这里用 floor 的原因是假设滚动了 39px 应该相当于滚动走了一个 item 而不是两个。上面的计算属性会根据 start 的变化重新计算出应该截取的数组,依赖于Vue会使操作更简单。

完整代码

<!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>Document</title>
  <style>
    * {
      margin: 0;
    }

    .container {
      overflow: auto;
      display: flex;
    }
  </style>
</head>
<div id="app">
  <div class="container" ref="container" :style="{ height: containerHeight }">
    <div class="empty" :style="{ height: emptyHeight }"></div>
    <ul class="list" :style="{ transform: `translateY(${translateY})` }">
      <li v-for="item in listData" :key="item" class="item" :style="{ height: itemHeight }">{{ item }}</li>
    </ul>
  </div>
</div>

<body>
  <script src="https://unpkg.com/vue@next"></script>
  <script>
    const { ref, onMounted, computed } = Vue
    const App = {
      setup() {
        const oriData = Array.from({ length: 1000 }, (v, k) => k)
        const itemHeight = 20
        const emptyHeight = itemHeight * oriData.length
        const containerHeight = window.innerHeight
        const itemCount = Math.ceil(containerHeight / itemHeight)
        const container = ref(null)
        const start = ref(0)
        const translateY = ref(0)
        const listData = computed(() => {
          return oriData.slice(start.value, start.value + itemCount + 1)
        })
        onMounted(() => {
          container.value.addEventListener('scroll', e => {
            const { scrollTop } = e.target
            start.value = Math.floor(scrollTop / itemHeight)
            translateY.value = scrollTop + 'px'
          })
        })
        return {
          listData,
          container,
          translateY,
          containerHeight: containerHeight + 'px',
          itemHeight: itemHeight + 'px',
          emptyHeight: emptyHeight + 'px'
        }
      }
    }
    Vue.createApp(App).mount('#app')
  </script>

</body>

</html>

写在后面

其实根据虚拟DOM的渲染规则,这个示例不给 itemkey 会比加 key 性能更好