原理:
- container 可视化区域高度,设置position:relative
- phantom 动态变化高度撑开容器,出现滚动条,设置position:absolute
- wrap 实际存放节点区域,设置position:absolute,动态设置transform:translate 始终显示在可视化区
- 利用container的scroll事件,在可视化startIndex变化时,渲染新节点
- 等渲染节点完成后,修改列表中每个数据的top,height,bottom属性,异步Promise.then重新计算transflorm高度
<!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>scroll实现虚拟列表</title>
<style>
#container {
height: 400px;
overflow: auto;
margin-top: 300px;
position: relative;
}
#phantom {
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
}
#wrap {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.child {
border: 1px solid red;
}
</style>
</head>
<body>
<div id="container">
<div id="phantom"></div>
<div id="wrap"></div>
</div>
<script>
// 预估高度
let estimatedHeight = 50,
startIndex = -1, // 开始
endIndex = 0, // 结束
bufferSize = 0 // 缓冲
const list = new Array(1000).fill().map((_, i) => {
return {
title: '标题' + i,
acHeight: 30 + Math.floor(Math.random() * 200),
el: null,
index: i,
height: estimatedHeight,
top: i * estimatedHeight,
bottom: i * estimatedHeight + estimatedHeight,
isUpdate: false
}
})
const container = document.querySelector('#container')
const wrap = document.querySelector('#wrap')
const phantom = document.querySelector('#phantom')
let visiableList = [],
visiableCount = Math.ceil(container.clientHeight / estimatedHeight)
function setWrapHeight() {
if (visiableList.length == 0) return
const lastItem = visiableList[visiableList.length - 1]
phantom.style.height =
lastItem.bottom +
estimatedHeight * (list.length - lastItem.index - 1) +
'px'
}
function updateStartEndIndex(scrollTop) {
const item = list.find(it => it.bottom >= scrollTop)
const index = item ? item.index : 0
if (index != startIndex) {
startIndex = index
endIndex = startIndex + visiableCount
return true
}
}
function handleScroll() {
container.addEventListener('scroll', function () {
// 更新索引
const isUpdate = updateStartEndIndex(container.scrollTop)
if (!isUpdate) return
renderDom()
})
}
function renderDom() {
let bufferStartIndex = Math.max(0, startIndex - bufferSize),
bufferEndIndex = Math.min(endIndex + bufferSize, list.length)
let bottom =
bufferStartIndex > 0 ? list[bufferStartIndex - 1].bottom : 0
visiableList = list.slice(bufferStartIndex, bufferEndIndex)
new Promise(resolve => {
// 先渲染
wrap.innerHTML = visiableList
.map(item => {
return `<div class='child' style='height:${item.acHeight}px;'>${item.title}</div>`
})
.join('')
resolve()
}).then(() => {
// 偏移tranfrom
wrap.style.transform = `translate3D(0,${bottom}px,0)`
// 再计算实际高度
const children = wrap.childNodes
visiableList.forEach((item, index) => {
item.top = bottom
item.height = children[index].offsetHeight
item.bottom = bottom + item.height
bottom = item.bottom
})
// 更新滚动高度
setWrapHeight()
})
}
updateStartEndIndex()
renderDom()
handleScroll()
</script>
</body>
</html>