虚拟滚动
背景-why
- 假设页面上有一个有大量数据的列表需要展示,每一行都是一个单独向项目,那么页面上就会有大量的dom元素
- 这些dom元素并不会完全的展示在页面上,只有视窗内的会被展示出来
- 存在大量的隐藏的dom元素,会造成页面的卡顿
什么是虚拟滚动-what
- 虚拟滚动是利用列表中有大量元素隐藏的特点,只将页面中能够展示的元素挂在dom中
- 页面发生滚动的时候,通过当前滚动的高度,来计算出当前应该在页面中展示的元素,然后将其挂载在dom中
- 概念
3. 内容项:单个展示项目
2. 窗口:当前页面中用来展示内容项的区域
- 容器:一个高度为所有内容项目总高度的元素,用来撑开窗口高度,容纳内容项
如何实现虚拟滚动-how
- 设置展示窗口和滚动元素及定义
<div class="viewport"> 窗口
<div class="content"></div> 容器
</div>
<style>
.viewport {
height: 300px;
overflow: auto;
}
.content {
height: 1000px;
padding: 20px;
}
</style>
<script>
const viewport = document.querySelector('.viewport');
const content = document.querySelector('.content');
const ITEM_HEIGHT = 50;
const ITEM_COUNT = 10000;
function createItem(index) {
const item = document.createElement('div');
item.classList.add('item');
item.textContent = `Item ${index}`;
item.style.height = `${ITEM_HEIGHT}px`;
return item;
}
</script>
- 渲染函数
function render(viewport, content) {
const scrollTop = viewport.scrollTop;
const viewportHeight = viewport.clientHeight;
const startIndex = Math.floor(scrollTop / ITEM_HEIGHT);
const endIndex = Math.min(
Math.ceil((scrollTop + viewportHeight) / ITEM_HEIGHT),
ITEM_COUNT
);
const items = [];
for (let i = startIndex; i < endIndex; i++) {
const item = createItem(i);
items.push(item);
}
content.innerHTML = '';
content.style.paddingTop = `${startIndex * ITEM_HEIGHT}px`;
content.append(...items);
}
- 监听窗口滚动事件
viewport.addEventListener('scroll', () => {
render(viewport, content)
});
- 整体demo
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>虚拟滚动实现</title>
<style>
.viewport {
height: 300px;
overflow: auto;
}
.content {
height: 1000px;
padding: 20px;
}
.item {
background-color: #eee;
border: 1px solid #ccc;
line-height: 50px;
padding: 10px;
}
</style>
</head>
<body>
<div class="viewport">
<div class="content"></div>
</div>
<script>
const viewport = document.querySelector('.viewport');
const content = document.querySelector('.content');
const ITEM_HEIGHT = 50;
const ITEM_COUNT = 10000;
const Total_Height = ITEM_HEIGHT * ITEM_COUNT
content.style.height = Total_Height
render(viewport, content)
viewport.addEventListener('scroll', () => {
render(viewport, content)
});
function createItem(index) {
const item = document.createElement('div');
item.classList.add('item');
item.textContent = `Item ${index}`;
item.style.height = `${ITEM_HEIGHT}px`;
return item;
}
function render(viewport, content) {
const scrollTop = viewport.scrollTop;
const viewportHeight = viewport.clientHeight;
const contentHeight = content.clientHeight;
const startIndex = Math.floor(scrollTop / ITEM_HEIGHT);
const endIndex = Math.min(
Math.ceil((scrollTop + viewportHeight) / ITEM_HEIGHT),
ITEM_COUNT
);
const items = [];
for (let i = startIndex; i < endIndex; i++) {
const item = createItem(i);
items.push(item);
}
content.innerHTML = '';
content.style.paddingTop = `${startIndex * ITEM_HEIGHT}px`;
content.append(...items);
}
</script>
</body>
</html>
更多优化思路
- 优化
- transform代替设置padding,可以有效利用gpu加速
- 现在滚动过程中每次都是在重新的生成所有的元素,但是页面上元素个数是固定的。滚动的过程中,如果元素一直在页面上,可以将其移动,对于消失的元素和新出现的元素将其内容替换,即将消失的元素的内容替换为新出现元素的内容,并将其移动到新出现元素的位置。这样可以避免新生成元素
- 上下额外渲染部分区域,可以在小范围滚动的时候优化体验
- 滚动节流,降低渲染频率
- 性能指标
- 浏览器渲染帧率
- 内存占用
- 重绘、重排