定高虚拟列表:让大数据渲染变得轻松
定高虚拟列表
基本认识
在数据如潮水般涌来的今天,如何高效地展示和管理这些数据成为了开发者们面临的一大挑战,传统的列表渲染方式在处理大量数据时,往往会导致页面卡顿、滚动不流畅等问题,严重影响用户体验(在页面渲染大量的 dom 时,不仅是在第一次渲染比较影响性能外,在后续的每一次回流或重绘时,也会造成巨大的性能的问题) - 我们需要知道 JS 执行永远要比 DOM 快的多
然而,有一种技术能够在不牺牲用户体验的前提下,显著提升大数据集的处理效率,它就是——虚拟列表
虚拟列表 实际上是一种实现方案,只对 可视区域
进行渲染,对 非可视区域
中的区域不渲染或只渲染一部分(渲染的部分叫 缓冲区
,不渲染的部分叫 虚拟区
),从而达到极高的性能
虚拟列表也分为定高虚拟列表与非定高虚拟列表等,定高虚拟列表的意思就就是说列表中每一项的高度需要是一样的,反之不定高就是列表中的每一项高度都是不缺订单 - 我们这里先来学一下定高虚拟列表的实现方式(后续再继续更新不定高虚拟列表的实现方式)
我们可以先通过一张图来简单分析定高虚拟列表
-
从图中可以看出,我们可以将列表分为三个区域:可视区、缓冲区、虚拟区
-
而我们主要针对
可视区
和缓冲取
进行渲染
基本实现
这里采用 vue3 来实现,如果使用其它框架的或原生,对应实现思路基本也是差不多的
HTML 页面布局
-
容器元素 - 我们需要有一个可视窗口的容器,可以是一整个屏幕视口(也可以通过指定对应容器 .virtual-list-container 的高度,并使该元素作为可视区域)
-
占位区域 - 在容器中我们还需要一个占位元素,用于撑开容器的高度使其容器可以存在对应的滚动条(因为容器中只渲染可视区域中的元素,所以我们还需要该占位元素来根据列表的数据量来撑出对应滚动条的高度)
-
内容区域 - 最后我们还需要有一个可视区域的容器,这块部分为真正用户看到的列表区域,对应渲染的数据实际上是
可视区域
加缓存区域
的列表数据→ tip: 缓冲区的作用是防止快速下滑或者上滑的过程中出现空白区域
-
<!-- 容器元素 --> <div class="virtual-list-container"> <!-- 占位元素 --> <div class="virtual-list-placeholder"></div> <!-- 渲染区域 --> <div class="virtual-list-content"> <!-- 子组件: 列表中的每一项 --> </div> </div> + tip: 这里只是一个简单的结构,因为后续还有需要在这些结构中进行一些动态的修改
在 JS 中我们需要的状态
-
list - 列表数据(所有数据)
-
showList - 所要真正展示的数据(在所有数据列表
list
中进行slice
裁剪而得,根据下面的start
与end
进行个数的裁剪) -
itemHeight - 列表中每一项的高度
-
renderCount - 可视区可以渲染的项目数量(根据
可视区域的高度 / itemHeight
计算出来,因为基本都会有小数问题的,可以通过 Math 来进行处理一下) -
bufferCount - 缓存区的项目个数
-
start - 截取展示数据的开始索引
-
end - 截取展示数据的结束索引(根据
start + renderCount + bufferCount
获取,但是可能会超出整个列表,所有需要根据该计算出来的值需要根据list.length
取最小值,避免 end 超出列表的数据量) -
currentOffset - 滚动偏移量,因为可视区域渲染的元素的个数是基本固定的,顺着滚动条的滚动必然也会向上滚动,所以可以根据该值对可视区域进行相应偏移量的平移,避免随着滚动条滚动(根据
滚动大小 - (滚动大小 % itemHeight)
进行获取)
代码实现:
-
HTML 结构
-
<template> <!-- 容器 --> <div class="virtual-list-container" ref="virtual-list-container"> <!-- 占位元素: 根据列表数据个数与每一项的大小计算出对应的高度 --> <div class="virtual-list-placeholder" :style="{ height: placeholderHeight + 'px' }"></div> <!-- 渲染区域: 根据对应的 currentOffset 偏移量,将渲染区域进行对应的平移 --> <div class="virtual-list-content" :style="{ transform: `translateY(${state.currentOffset}px)` }"> <!-- 子组件: 循环列表中的每一项 --> <div class="item" v-for="item in showList" :key="item">{{ item }}</div> </div> </div> </template>
-
-
CSS 样式
-
<style scoped lang="scss"> .virtual-list { &-container { /* 容器大小 */ width: 100%; height: 100vh; position: relative; overflow: auto; } &-placeholder { position: absolute; inset: 0; } &-content { position: absolute; inset: 0; } } .item { /* item 列表项样式 */ outline: 1px solid orange; height: 60px; line-height: 60px; text-align: center; } </style>
-
-
JS 部分
-
<script setup> import { computed, onMounted, reactive, useTemplateRef } from 'vue'; const list = new Array(20000).fill(null).map((item, i) => i + 1) // -- 模拟列表数据(所有数据) const containerRef = useTemplateRef("virtual-list-container") // -- 容器元素 // -- 获取相应的状态 const state = reactive({ start: 0, // -- 开始索引 itemHeight: 60, // -- 每项 item 中的高度 renderCount: 0, // -- 可视区域可以渲染的项目个数(根据容器高度与 itemHeight 进行计算) bufferCount: 6, // -- 缓存个数 currentOffset: 0, // -- 根据滚动距离获取对应的偏移量 }) const end = computed(() => // -- 计算结束索引 Math.min( state.start + state.renderCount + state.bufferCount, list.length ) ) const showList = computed(() => list.slice(state.start, end.value)) // -- 根据 start 与 end 获取对应需要展示的列表数据 onMounted(() => { const containerHeight = containerRef.value.offsetHeight // -- 获取容器高度 state.renderCount = Math.round(containerHeight / state.itemHeight) // -- 获取可视区域可渲染个数 }) const placeholderHeight = list.length * state.itemHeight // -- 占位元素的高度(用于给容器撑出对应的滚动条) </script>
-
-
上面的代码已经可以根据对应的视口大小,来计算所需要渲染的个数了,但是还有一个滚动时的处理,为了方便大家阅读,所以将滚动条滚动部分的代码放在这下面(这些代码直接与上面
JS 部分
进行合并即可)-
const handleScrollEvent = () => { // -- 触发滚动事件时,所需要处理的操作 if (!containerRef.value) return const { scrollTop } = containerRef.value // -- 根据滚动大小,重新计算 start 开始索引(因为 end 结束索引是根据该 start 派生的计算属性,所以也会自动的进行计算) state.start = Math.round(scrollTop / state.itemHeight) // -- 根据滚动大小,重新计算对应滚动的偏移量,用于动态的给可视区域进行对应偏移量的平移 state.currentOffset = scrollTop - (scrollTop & state.itemHeight) } onMounted(() => { if (!containerRef.value) return containerRef.value.addEventListener("scroll", handleScrollEvent) // -- 监听容器的滚动 }) onUnmounted(() => { if (!containerRef.value) return containerRef.value.removeEventListener("scroll", handleScrollEvent) // -- 组件卸载时取消监听滚动事件 })
-
-
通过上面的代码我们就可以简单的实现了一个定高的虚拟列表了
注意:
- 上面的代码虽然实现了定高的虚拟列表,单滚动条的触发频率我们可以通过稍加一点点
节流(Throttle)
获防抖(Debounce)
来降低回调函数的触发频率(当然使用防抖还是节流具体看你想要的是一种怎么样的效果) 当然,我们也可以通过 Intersection Observer API 来代替滚动事件的监听,来提高对应的性能
总结: 定高虚拟列表实现思路
1. 在 HTML 结构中的处理
-
(1) - 需要有一个可视窗口的容器
-
(2) - 容器中需要有一个占位元素,用来撑开视口高度,使其能够有对应高度的滚动条(
该元素的高度需要等于: 长列表中的数据长度 * 每个元素的高度
→ tip: 如果元素存在 margin 等也需要计算上去
) -
(3) - 容器中还需要有一个内容区域(用来存放真正渲染的列表数据),该元素需要是一个根据容器元素的一个绝对定位元素(
用于当滚动条在滚动时,该内容区域元素可以根据定位中的 top 属性进行相应滚动距离的平移,避免整个容器区域也随着滚动上去(当然也可以通过 transform 进行平移)
)
2. 在 JS 中的处理
-
list - 列表数据(所有数据)
-
showList - 所要真正展示的数据(在所有数据列表
list
中进行slice
裁剪而得,根据下面的start
与end
进行个数的裁剪) -
itemHeight - 列表中每一项的高度
-
renderCount - 可视区可以渲染的项目数量(根据
可视区域的高度 / itemHeight
计算出来,因为基本都会有小数问题的,可以通过 Math 来进行处理一下) -
bufferCount - 缓存区的项目个数
-
start - 截取展示数据的开始索引
-
end - 截取展示数据的结束索引(根据
start + renderCount + bufferCount
获取,但是可能会超出整个列表,所有需要根据该计算出来的值需要根据list.length
取最小值,避免 end 超出列表的数据量) -
currentOffset - 滚动偏移量,因为可视区域渲染的元素的个数是基本固定的,顺着滚动条的滚动必然也会向上滚动,所以可以根据该值对可视区域进行相应偏移量的平移,避免随着滚动条滚动(根据
滚动大小 - (滚动大小 % itemHeight)
进行获取)
3. onscroll 监听滚动条 : 在滚动条监听的回调中,主要需要做如下两件事
- (1) - 获取当前的滚动大小,并根据该值与对应的 itemHeight 大小计算出新的 start,并赋值该对应的 start 状态中
- (2) - 根据滚动大小,动态设置 HTML 中的列表容器(.virtual-list)中的 top 样式,进行对应的平移 → 避免列表容器也随着滚动条的滚动而进行滚动