为什么
前端一次性展示很多数据的时候因为回流和重绘会带来比较大的性能问题,因此需要一种方法减少渲染的性能开销。
假设后端返回 1000 条数据,前端一次性展示出来,可能用户只想看到前 100 条,那么剩下 90% 的数据就没必要展示了。这一点可以用分页处理,也可以用虚拟列表。
另外在无限滚动的页面中,也可能会有大量的数据存在前端展示,随着滚动深度加深,展示的数据也越来越多,就可能造成上面提到的性能问题。
怎么做
虚拟列表的核心就是只渲染用户能看到的数据,无论实际有多少条数据。
假设视口高度为 clientHeight
,元素高度为 itemHeight
,则可以得出如下关系:
- 视口能展示的元素个数
itemCount = clientHeight / itemHeight
- 页面偏移量(有多少个元素已经展示,这部分元素我们需要移除):
offset = scrollTop / itemHeight
- 数据列表切片范围:
start = offset; end = start + itemCount
- 页面高度:
contentHeight = data.length * itemHeight
因为我们实际上只展示一个视口能看到的元素,所以实际上页面是不会有很大的滚动条(或者没有滚动条)的,所以这里需要一个撑起页面高度的元素,这个元素的高度为 contentHeight
,我们要展示的元素则是通过 scrollTop
offset 到滚动条的位置。
源码
只考虑了最基本的情况:元素高度是固定的,一般用来展示图片之类的。更多情况可以参考:juejin
<!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>
body {
margin: 0;
background-color: #ccc;
}
.virtual-list-content {
position: absolute;
top: 0;
}
.list-item {
height: 200px;
margin-bottom: 1px;
background-color: azure;
width: 200px;
font-size: 5rem;
}
</style>
</head>
<body>
<div class="virtual-list-block"></div>
<div class="virtual-list-content"></div>
<script>
const data = Array.from({ length: 1e7 }).map((_, idx) => idx)
const itemHeight = 200;
const listHeight = 400;
const block = document.querySelector('.virtual-list-block')
const content = document.querySelector('.virtual-list-content')
block.style.height = data.length * itemHeight + 'px'
function renderItems(startIndex, endIndex) {
const frg = document.createDocumentFragment()
data.slice(startIndex, endIndex).forEach(item => {
const div = document.createElement('div')
div.innerText = item
div.className = 'list-item'
frg.appendChild(div)
})
content.replaceChildren(frg)
}
window.addEventListener('scroll', (e) => {
const { scrollTop } = document.documentElement
let startIndex = Math.floor(scrollTop / itemHeight), endIndex = startIndex + Math.floor(window.innerHeight / itemHeight)
content.style.transform = `translateY(${(scrollTop - (scrollTop % itemHeight))}px)`
renderItems(startIndex === 0 ? 0 : startIndex - 1, endIndex + 1)
})
</script>
</body>
</html>