一、常见的长列表解决方案
对于长列表,解决方案往往有以下几种:
- 分页加载【PC端常用】
- 无限列表 vue-infinite-scroll 【掘金就是这样用的】
优点:不需要考虑恢复问题 + 置顶问题,在列表不多的情况下不会存在卡顿的情况;
缺点:随着DOM渲染越来越多,占用内存越多,后面回越来越卡顿
3. 虚拟列表
思想:只渲染可视窗口之内的UI;
优点:流畅,体验感好;
缺点:必须要数据恢复 + 可能存在抖动;
二、虚拟列表
Github Demo在这儿
总的来说,虚拟列表的思想也就是只渲染当前用户可以见到的窗口,一般Item分为固定高度和非固定高度两种情况;
<head>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0px;
}
.container {
width: 100%;
height: 100vh;
overflow: auto;
}
.item {
background-color: darkgray;
width: 100%;
border: thick solid darkgreen;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="virtual_list"></div>
</div>
</body>
1)固定高度js实现方案
const container = document.querySelector('.container')
const virtualList = document.querySelector('.virtual_list')
const dataSource = Array.from({ length: 100 }, (v, i) => i )
const ITEM_HEIGTH = 100;
const SCROLL_CONTENT_HEIGHT = ITEM_HEIGTH * (dataSource.length); // 滚动区域大小
const CONTAINER_HEIGHT = container.clientHeight; //滚动窗口高度;
const ITEM_COUNT = Math.ceil(CONTAINER_HEIGHT/ITEM_HEIGTH); // 当前视图show出来的数量
function showCurrentList(target) {
let scrollTop = target.scrollTop; // 当前视图的scrollTop
let startIdx = Math.floor(scrollTop / ITEM_HEIGTH); // 开始值;
let endIdx = startIdx + ITEM_COUNT;
let itemList = '';
const paddingTop = startIdx * ITEM_HEIGTH
virtualList.setAttribute('style', `padding-top:${paddingTop}px;`)
for (let i = startIdx; i < endIdx; i++) {
const element = dataSource[i];
itemList += `<div class="item" style="height:${ITEM_HEIGTH}px">${element}</div>`
}
// TODO: 此处直接用innerHTML,还可以用Document Fragment;
// 同样是避免单次重复渲染的方式;
virtualList.innerHTML = itemList;
}
showCurrentList(container)
container.addEventListener('scroll', (event) => {
showCurrentList(event.target)
})
2)非固定高度js实现方案
const container = document.querySelector('.container')
const virtualList = document.querySelector('.virtual_list')
const dataSource = Array.from({ length: 100 }, (v, i) => { return { idx: i, height: Math.floor(Math.random() * (70 - 0) + 40) } })
const SCROLL_CONTENT_HEIGHT = getScrollContentHeight(dataSource); // 滚动区域大小
const CONTAINER_HEIGHT = container.clientHeight; //滚动窗口高度;
function getScrollContentHeight(dataSource) {
let pre = 0;
dataSource.forEach(item => {
item.position = pre + item.height;
pre = item.position; // 记录当前item的终止位置;
});
return dataSource[dataSource.length - 1].position;
}
function showCurrentList(target) {
let scrollTop = target.scrollTop; // 当前视图的scrollTop
// 找到data中的position大于scrollTop的第一个值 -- startIndex;
// 找到data中position大于scrollTop + CONTAINER_HEIGHT的第一个值 -- endIndex;
// TODO: 寻找startIdx和endIdx存在优化解法,包括二份,Cache缓存等方式;
let itemList = '';
let satrt = dataSource.find(e => e.position > scrollTop);
virtualList.setAttribute('style', `padding-top:${satrt.position - satrt.height}px;`)
for (let i = satrt.idx; i < dataSource.length; i++) {
const element = dataSource[i];
if (element.position > CONTAINER_HEIGHT + scrollTop) break;
itemList += `<div class="item" style="height:${element.height}px">${element.idx}</div>`
}
virtualList.innerHTML = itemList;
}
showCurrentList(container)
container.addEventListener('scroll', (event) => {
showCurrentList(event.target)
})
3)固定高度 + 缓冲区
为了避免上下滑动的时候,出现卡顿白屏等加载不及时的情况,给上下区域都增加一个缓冲区padding;
【如果滑动的实在太快,其实卡顿白屏的情况其实是难以避免的】
const container = document.querySelector('.container')
const virtualList = document.querySelector('.virtual_list')
const dataSource = Array.from({ length: 100 }, (v, i) => i )
const ITEM_HEIGTH = 100;
const SCROLL_CONTENT_HEIGHT = ITEM_HEIGTH * (dataSource.length); // 滚动区域大小
const CONTAINER_HEIGHT = container.clientHeight; //滚动窗口高度;
const ITEM_COUNT = Math.ceil(CONTAINER_HEIGHT/ITEM_HEIGTH); // 当前视图show出来的数量
const PADDING_ITEM = 5
function showCurrentList(target) {
let scrollTop = target.scrollTop; // 当前视图的scrollTop
let startIdx = Math.floor(scrollTop / ITEM_HEIGTH); // 开始值;
let endIdx = startIdx + ITEM_COUNT + PADDING_ITEM;
let itemList = '';
const paddingTop = startIdx * ITEM_HEIGTH
virtualList.setAttribute('style', `padding-top:${paddingTop}px;`)
startIdx = startIdx - PADDING_ITEM > 0 ? startIdx - PADDING_ITEM : startIdx
endIdx = endIdx > dataSource.length ? dataSource.length : endIdx
for (let i = startIdx; i < endIdx; i++) {
const element = dataSource[i];
itemList += `<div class="item" style="height:${ITEM_HEIGTH}px">${element}</div>`
}
virtualList.innerHTML = itemList;
}
showCurrentList(container)
container.addEventListener('scroll', (event) => {
showCurrentList(event.target)
})
4)CSS新属性
content-visibility:auto;
主流的Chrome和Edge支持,但不支持的浏览器还是很多。
作用:如果该元素不在屏幕上,并且与用户无关,则不会渲染其后代元素。
三、可以直接用的npm包
React: react-window
四、存在问题:
-
滚动太快会出现白屏
增加滚动缓存区【其实如果太快还是会卡顿了,但这个没办法, 而且其实这种问题用户也不是不可以接受,很多app都是变下拉便拉数据,无论如何都可以白屏的】;
-
DOM过多 + 网络请求仍旧会卡
配合骨架屏食用