虚拟列表:简而言之,对于大量列表数据,仅渲染滚动区域内可视区域 所对应 数据源 的部分数据
举个例子:
比如我要渲染1000条数据,每条数据高度60px,如果我想把全部元素渲染,那么滚动区域至少需要6000px高但是呢,如果我渲染1000条数据,那么整个dom树就会出现巨量dom元素,如下图,B,C,D区域将全部都是dom元素,一旦回流重绘就会非常卡顿。
这种场景的优化方案就是采取虚拟列表去渲染,即我们要做的就是在滚动的时候只渲染可视区域对应的dom元素,即下图的B,非可视区域内元素不去渲染(即C,D区域不渲染元素),这样的话可以减少大量非必要DOM渲染(C,D区域用户也不需要看见,所以它的dom渲染是非必要的),去提高性能!
虚拟列表实现原理:根据视口高度(上图中的B区域高度),父元素的滚动距离(scrollTop)及每一个列表元素item的平均高度,计算出当前scrollTop到scrollTop+视口高度这段可视区域 对应数据源列表所需要展示的 部分数据,再将这部分数据展示在B区域内,对于隐藏滚动区域C,D使用paddingTop&&paddingBottom填充,保持整个滚动区域高度不变,同时监听滚动,滚动触发时动态改变可是区域内数据及paddingTop&&paddingBottom等数据:
-
1,监听容器元素滚动
-
2,获取容器元素滚动距离scrollTop
-
3,获取当前滚动距离(scrollTop)下可视区域首个需要渲染元素索引(in 数据源)startIndex,计算方式:startIndex = 滚动距离(scrollTop)/ 列表元素高度(itemHeight)
-
4,获取当前滚动距离(scrollTop)下可视区域最后一个需要渲染元素索引(in 数据源)endIndex,计算方式:endIndex = startIndex + 可视区域高度(viewHeight)/ 列表元素高度(itemHeight)
-
5,根据startIndex和endIndex获取数据源对应的 可视区域需要展示的数据viewData,计算方式:viewData =dataSource(数据源).slice(startIndex,endIndex+1)
-
6,计算C区域的高度,既paddingTop,计算方式:paddingTop = startIndex*itemHeight(列表元素高度)
-
7,计算D区域的高度,既paddingBottom,计算方式:paddingBottom = contentHeight(整个滚动区域高度,既A区域)-paddingTop-可视区域高度(viewHeight)
-
8,清除可是区域内上一次展示的数据,使用新数据viewData重新填充,且设置新的paddingTop&paddingBottom
代码实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
}
#container {
width: 100%;
height: 100%;
}
#content {
background-color: pink;
}
</style>
</head>
<body>
<div id="container">
<div id="content"></div>
</div>
<script>
function createVlist(container, content, data_Source, item_Height) {
const dataSource = data_Source // 数据源
const itemHeight = item_Height // 每一行元素高度
const viewHeight = container.clientHeight // 视口高度(虚拟列表所展示部分高度)
const contentHeight = itemHeight * dataSource.length // 滚动区域高度
const itemCount = Math.ceil(viewHeight / itemHeight) // 视口元素数量
// 设置容器元素overflow: auto (生成滚动区域,不被截取)
container.setAttribute('style', `overflow:auto`)
// 设置滚动区域高度
content.setAttribute('style', `height:${contentHeight}px`)
const scrollCallback = e => {
const scrollTop = e.target.scrollTop // 容器元素的滚动距离
const startIndex = Math.ceil(scrollTop / itemHeight) // 视口第一个元素所在数据源中的索引
const endIndex = startIndex + itemCount // 视口最后一个元素所在数据源中的索引
// 根据startIndex endIndex找出数据源中需要展示在虚拟列表中的部分数据
const itemList = dataSource.slice(startIndex, endIndex + 1)
// 滚动区域高度contentHeight = 滚动区域paddingTop + 视口高度 + 滚动区域paddingBottom
const paddingTop = startIndex * itemHeight
const paddingBottom = contentHeight - paddingTop - itemCount * itemHeight
// 动态调整滚动区域的paddingTop paddingBottom 保证列表部分始终展示在视口
content.setAttribute('style', `padding-top:${paddingTop}px;padding-bottom:${paddingBottom}px`)
// 展示下一批次列表前删除上一批次列表数据
content.innerHTML = ''
// 动态调整每次滚动后对应的展示数据
for (const val of itemList) {
const item = document.createElement('div')
item.innerHTML = val
item.setAttribute('style', `background-color:${val % 2 === 0 ? 'red' : 'blue'};width:100%;height:${item_Height}px`)
content.appendChild(item)
}
}
// 添加容器元素的滚动监听
container.addEventListener('scroll', scrollCallback)
// 初始首屏数据
scrollCallback({ target: container })
}
// 创建虚拟列表
createVlist(container, content, Array.from({ length: 100 }, (v, i) => i), 60)
</script>
</body>
</html>