虚拟列表是前端解决海量数据展示的一种解决方案。
当我们需要展示万条,百万条数据时。如果使用传统的分页向下展示。随着数据量的增多,HTML节点也会增加,HTML节点越多,重绘
和重排
的花销也会增大,慢慢地会让你的页面变得非常的慢。
那么为什么使用虚拟列表就能解决这个问题呢?
虚拟列表的原理
虚拟列表的原理其实就是:当我们查询出大量数据时,只展示当前可视区域的数据,其他的数据只在滚动到数据的页数时才展示。如图所示:

我们可以预先加载几页数据,当我们滚动到预加载的页面时,加载下(上)一页数据,并删除上(下)一页数据。这样无论我们怎么滚动,页面中展示的数据量始终保持固定的。
实现虚拟列表
了解完它的原理,接下来就是需要实现这个虚拟列表,这里可以提供两种方式解决问题~
因为ResizeObserver
是实验性的API,不推荐在生产环境中使用,所以我们这里第一种方法:监听滚动来实现。
具体的实现步骤为:
- 创建容器并监听容器滚动
- 获取容器高度和每个列表的高度
- 计算触发下(上)一页触发的滚动距离
- 计算留白的高度(重要)
其中,计算留白的高度是虚拟列表最重要的一环,因为当元素隐藏时,为了保证展示列表不出现塌陷,需要使用padding
将容器撑高。
虚拟列表有定高
和不定高
两种。
- 定高就是每一个item固定高度,我们在计算滚动距离的时候就会比较轻松。
- 但是很多情况下,列表的item高度是不固定的,这时我们就要比定高多一步:计算每一页渲染后动态计算的总高度。
定高虚拟列表
首先新建一个html文件,创建一个box容器,固定容器高度并监听滚动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <!DOCTYPE html> <html> <head> <title>虚拟列表</title> <meta charset="utf8" /> <style> * { padding: 0; margin: 0; } body { width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; } #box { width: 300px; height: 500px; background: purple; overflow-y: scroll; box-sizing: border-box; } </style> </head> <body> <div id="box"> <div id="box_container"></div> </div> </body> <script lang="text/javascript" src="./script.js"></script> </html>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| (() =>{ let page = 1; let size = 20; const height = 50; const preLoadNum = 3; const boxHeight = 500; let paddingBottom = 50; let listArr = []; const box = document.querySelector("#box"); box.addEventListener("scroll", (e) => {}, false) })()
|
定义完一些基础的初始信息之后,接下来写一个获取数据的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
function createItem(page = 1, size = 10) { const fragment = document.createDocumentFragment() const box = document.createElement("div"); box.className = `page_${page}`; for (let i = 0; i < size; i++) { const element = document.createElement("div");
element.style.width = "100%"; element.style.height = "50px"; element.style.color = "#fff"; element.className = `item_${page * (i + 1)}`; element.innerText = `我是item——${((page - 1) * size) + i + 1}`; box.appendChild(element); } fragment.appendChild(box); return {fragment, box}; }
|
回到立即执行函数中,进入页面时立即执行一遍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
(() =>{ let page = 1; let size = 20; const height = 50; const preLoadNum = 3; const boxHeight = 500; let listArr = [];
const box = document.querySelector("#box"); const boxContainer = document.querySelector("#box_container"); const {fragment, box: boxList} = createItem(page, size) listArr.push(boxList); boxContainer.appendChild(fragment); box.addEventListener("scroll", (e) => { }, false) })()
|
接下来就需要将计算滚动高度去获取并展示数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| box.addEventListener("scroll", (e) => { const scrollTop = e.target.scrollTop if (scrollTop >= nextHeight) { page++; paddingTop = (page - preLoadNum) * (size * height) + paddingBottom; nextHeight = (page - 1) * (size * height) + paddingBottom + boxHeight; let fragment; if (!listArr[page - 1]) { const {fragment: element, box: boxList} = createItem(page, size) fragment = element; listArr.push(boxList); } else { fragment = listArr[page - 1] } boxContainer.appendChild(fragment); const hideElem = document.querySelector(`.page_${page - preLoadNum}`); if (hideElem) { boxContainer.removeChild(hideElem); boxContainer.style.paddingTop = `${paddingTop}px`; } } else if ( scrollTop <= (page - preLoadNum + 1) * size * height + paddingBottom && page > preLoadNum ) { page--; paddingTop = (page - preLoadNum) * (size * height) + paddingBottom; nextHeight = (page - 1) * (size * height) + paddingBottom + boxHeight; const fragment = listArr[page - preLoadNum]; boxContainer.insertBefore(fragment, boxContainer.childNodes[0]); const hideElem = document.querySelector(`.page_${page + 1}`); if (hideElem) { boxContainer.removeChild(hideElem); boxContainer.style.paddingTop = `${paddingTop}px`; } } }, false)
|
大功告成!来测试一下效果
传送门
不定高虚拟列表
对于不定高的虚拟列表,我们需要计算每一页的总高度,或者将每一页的每一项高度都加上然后累加去,因为我们每一页都用了一个容器来将他们保存起来,并且容器是自动撑高的,所以,只需要获取每一页容器的高度即可。修改一下上面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| function createItem(page = 1, size = 10) { const fragment = document.createDocumentFragment() const box = document.createElement("div"); box.className = `page_${page}`; for (let i = 0; i < size; i++) { const element = document.createElement("div"); let height = Math.ceil(Math.random() * 5) * 50 element.style.width = "100%"; element.style.height = `${height}px`; element.style.color = "#fff"; element.className = `item_${page * (i + 1)}`; element.innerText = `我是item——${((page - 1) * size) + i + 1} \n 高度——${height}`; box.appendChild(element); } fragment.appendChild(box); return {fragment, box}; } (() => { let page = 1; let size = 20; let height = 50; let preLoadNum = 3; let boxHeight = 500; let paddingBottom = 50; const box = document.querySelector("#box"); const boxContainer = document.querySelector("#box_container"); let listArr = []; let isGetting = false;
const {fragment, box: boxList} = createItem(page, size) boxContainer.appendChild(fragment);
const listHeight = document.querySelector(`.page_${page}`).clientHeight; listArr.push({boxList, height: listHeight});
let paddingTop = 0;
let nextHeight = paddingTop + listHeight + paddingBottom - boxHeight;
boxContainer.style.paddingBottom = `${paddingBottom}px`; boxContainer.style.paddingTop = `${paddingTop}px`;
box.addEventListener("scroll", (e) => {
const scrollTop = e.target.scrollTop if (scrollTop >= nextHeight) { if (isGetting) return; isGetting = true; page++; let fragment; let pushObj; if (!listArr[page - 1]) { const {fragment: element, box: boxList} = createItem(page, size) fragment = element; pushObj = { boxList }; } else { const { boxList, height } = listArr[page - 1] fragment = boxList; }
boxContainer.appendChild(fragment); paddingTop = (listArr.filter((_, index) => index < page - preLoadNum)).map((val) => val.height).reduce((a,b) => (a + b), 0); nextHeight = boxContainer.clientHeight - boxHeight - paddingBottom; if (pushObj) { const listHeight = document.querySelector(`.page_${page}`).clientHeight; pushObj.height = listHeight; listArr.push(pushObj) }
const hideElem = document.querySelector(`.page_${page - preLoadNum}`); if (hideElem) { boxContainer.removeChild(hideElem); boxContainer.style.paddingTop = `${paddingTop}px`; } } else if (scrollTop <= nextHeight - listArr[listArr.length - 1].height && page > preLoadNum) { page--; const { boxList } = listArr[page - preLoadNum]; boxContainer.insertBefore(boxList, boxContainer.childNodes[0]);
const hideElem = document.querySelector(`.page_${page + 1}`); if (hideElem) { paddingTop = (listArr.filter((_, index) => index < page - preLoadNum)).map((val) => val.height).reduce((a,b) => (a + b), 0); boxContainer.style.paddingTop = `${paddingTop}px`; boxContainer.removeChild(hideElem); nextHeight = boxContainer.clientHeight - boxHeight - paddingBottom; } } isGetting = false; }, false); })()
|
最后实现效果传送门
总结
本文主要介绍了
- 虚拟列表的原理
- 虚拟列表的实现步骤
- 定高的虚拟列表实现
- 不定高的虚拟列表实现
参考