前言
之前实现了一个定高版本的虚拟列表,今天在定高版本的基础上稍作调整,来实现不定高版本,之前的版本请跳转对应文章查看:JavaScript原生实现简单虚拟列表(固定高度) 。
先说结论
实现不定高的原理就是:每次把内容渲染到页面后,都去重新获取一次 item 的实际高度,然后再执行一次渲染。
最开始想用 createDocumentFragment
创建一个虚拟的文档节点,先将内容渲染到这个虚拟的文档节点中,最后从虚拟文档节点中获取 item 的实际高度,最后,一次插入到真实DOM中。结果发现,在虚拟的文档节点中时拿不到 item 的实际高度的,所以才有了下面的实现方式。
完整代码
不定的高版本代码如下:
html 和 css 与定高版本相比,未做任何调整
<div class="container">
<div class="zhanwei"></div>
</div>
<style>
.container {
border: 1px solid #eee;
height: 300px;
width: 300px;
overflow: auto;
position: relative;
box-sizing: border-box;
}
.zhanwei {
position: relative;
}
.item {
position: absolute;
top: 0;
min-height: 50px;
width: 100%;
border: 1px solid #eee;
will-change: transform;
box-sizing: border-box;
}
.item:nth-of-type(odd) {
background: #00ccff;
}
.item:nth-of-type(even) {
background: #ffcc00;
}
</style>
js 代码如下
和定高版本相比,就两处改动,请查看代码中的 变化一
和 变化二
;
// 不固定高度版本
let container = document.querySelector('.container');
let zhanwei = document.querySelector('.zhanwei');
let itemList = []; // 假设有10000条数据
for (let i = 0; i < 10000; i++) {// 生成10000条数据
itemList.push({
index: i,
content: `Item ${i} - ${"Hello world!".repeat(Math.floor(Math.random() * 10))}`
});
};
let buffer = 5; // 多渲染几条,避免滚动看着异常
let itemHeight = 50; // 每条数据的一个默认最小高度
let heights = new Map();// 记录渲染的每个 item 的高度,为不定高版本做准备
let offsets = new Map(); // 记录每个 item 的偏移量,即每个item距离顶部的距离
let rendered = new Map(); // 存储已渲染的数据
// 更新偏移量, 根据item高度,计算 zhanwei 元素的高度,好让container出现滚动条
function updateOffsets() {
let offset = 0
for (let i = 0; i < itemList.length; i++) {
let h = heights.get(i) ?? itemHeight; // ?? 是空值合并运算符,当左边为null或者undefined时使用右边值,和三元运算符相比,排除了 0 的干扰
offsets[i] = offset;
offset += h + 5; // 加上了5个像素的间距
}
zhanwei.style.height = offset + 'px';
}
// 变化一:创建一个重新渲染函数
function rerender(item, i) {
let height = item.getBoundingClientRect().height
if (heights.get(itemList[i].index) !== height) {
heights.set(itemList[i].index, height)
updateOffsets()
render()
}
}
// 渲染数据
function render() {
let scrollTop = container.scrollTop;
let viewHeight = container.clientHeight;
let start = 0; // 查找视口第一个item的索引
while (start < itemList.length && offsets[start + 1] < scrollTop) {
start++;
}
let end = start ;// 查找视口最后一个item的索引
while (end < itemList.length && offsets[end] < scrollTop + viewHeight) {
end++;
}
start = Math.max(0, start - buffer);
end = Math.min(itemList.length, end + buffer);
let nextRendered = new Map(); // 当前需要渲染的数据
for (let i = start; i < end; i++) {
if (!rendered.has(i)) {
let item = document.createElement('div')
item.className = 'item'
item.style.transform = `translateY(${offsets[itemList[i].index] + 'px'})`
item.textContent = itemList[i].content
container.appendChild(item)
rendered.set(i, item)
// 变化二:向页面插入数据后执行一次重新渲染
rerender(item, i)
}
nextRendered.set(i, rendered.get(i))
}
// 不可见的区域 移除
for (const [i, el] of rendered.entries()) {
if (!nextRendered.has(i)) {
container.removeChild(el);
}
}
// 更新 rendered
rendered.clear();
for (const [i, el] of nextRendered.entries()) {
rendered.set(i, el);
}
}
container.addEventListener("scroll", render);
updateOffsets()
render()
如果你有更好的实现方案,欢迎留言、贴代码交流。
感谢你的阅读 ❤️