渲染
一次性渲染大量数据不卡顿
原理: 时间换空间,使用 requestIdleCallback
或者 requestAnimationFrame
调度渲染。 缺点: 大量dom需要占用较多内存
<body>
<ul id="list"></ul>
<script>
function render() {
const FRAME_TIME = 1000 / 60; // 定义每帧的时长
const TOTAL_ITEMS = 100000; // 总条数
const ITEMS_PER_RENDER = 20; // 每次渲染20条
const RENDER_COUNT = TOTAL_ITEMS / ITEMS_PER_RENDER;
const list = document.getElementById('list');
let renderedCount = 0;
function renderItem(deadline) {
if (deadline ? deadline.timeRemaining() > 0 : true) {
// 1. 使用文档碎片优化一次插入优化性能
const fragment = document.createDocumentFragment();
for (let i = 0; i < ITEMS_PER_RENDER; i++) {
const item = document.createElement('li');
item.innerText = Math.floor(Math.random() * TOTAL_ITEMS);
fragment.appendChild(item);
}
list.appendChild(fragment);
renderedCount += 1;
}
loop();
}
function loop() {
// 使用 requestIdleCallback 或者 requestAnimationFrame 函数循环渲染 li 元素,避免卡顿
if (renderedCount < RENDER_COUNT) {
// 2. 兼容性处理,使用 requestIdleCallback 或 requestAnimationFrame
if ('requestIdleCallback' in window) {
window.requestIdleCallback(renderItem);
} else {
scheduleAnimationTask(renderItem);
}
}
}
function run() {
if ('requestIdleCallback' in window) {
window.requestIdleCallback(renderItem);
} else {
scheduleAnimationTask(renderItem);
}
}
function scheduleAnimationTask(callback) {
let startTime = Date.now();
requestAnimationFrame(() => {
if (Date.now() - startTime < FRAME_TIME) {
callback();
} else {
scheduleAnimationTask(callback);
}
});
}
run();
}
render()
</script>
</body>
虚拟列表实现
原理:按需渲染, 只渲染视口范围内可见的元素。 步骤:
初始化:每一项高度预估一个默认值,用一个元素如这里的scrollHold元素撑开滚动条高度。
根据滚动高度确定视口元素的起始位置start,计算视口的区间元素位置positions[start,end]
根据start计算滚动内容容器的偏移量
currentOffset
利用css的transform
设置滚动内容容器的偏移量渲染后计算实际内容高度记录下来,更新虚拟滚动条内容高度。
TODO:
使用骨架屏作为缓冲区。
算法上的优化缓存高度等。
滚动获取数据,请求过程中显示骨架屏。数据渲染显示数据隐藏骨架屏。
<style>
/* 列表项样式 */
.list-item {
list-style: none;
background-color: #fc4838;
padding: 10px 20px;
color: #fff;
height: 50px;
display: flex;
justify-content: center;
box-sizing: border-box;
align-items: center;
margin: 10px 24px;
font-weight: bold;
border-radius: 10px;
}
.app {
height: 80vh;
padding: 0;
margin-top: 50px;
overflow: hidden;
}
.list-box {
position: fixed;
left: 0;
top: 60px;
right: 0;
bottom: 0;
}
/* 滚动占位元素样式 */
.scroll-hold {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.scroll-box {
height: 70vh;
overflow-y: auto;
position: relative;
}
</style>
<body>
<div class="app" id="box">
<div id="scroll" class="scroll-box" onscroll="handleScroll()">
<div class="scroll-hold" id="scrollHold"></div>
<div class="skeleton-box" id="skeletonBox"></div> <!-- TODO: -->
<div class="context" id="context"></div>
</div>
</div>
<script>
let dataList = []; // 数据列表
let position = [0, 0]; // 当前渲染列表项的起始位置和结束位置
const scroll = document.getElementById('scroll'); // 滚动容器
const context = document.getElementById('context'); // 实际内容
const scrollHold = document.getElementById('scrollHold'); // 滚动内容容器,用来撑开滚动条
const scrollInfo = { // 滚动相关信息
height: 500, // 滚动容器高度
bufferCount: 4, // 缓冲区数量
defaultItemSize: 60, // 列表项高度
renderCount: 0, // 当前渲染的列表项数量
};
function run() {
const height = scroll.offsetHeight; // 获取滚动容器高度
const { defaultItemSize, bufferCount } = scrollInfo;
const renderCount = Math.ceil(height / defaultItemSize) + bufferCount; // 计算每次渲染的列表项数量
scrollInfo.renderCount = renderCount;
let length = 1000
dataList = new Array(length).fill(1).map((item, index) => {
return {
id: index + 1,
height: defaultItemSize,
}
}); // 初始化数据列表
const initScrollHold = dataList.length * (defaultItemSize)
scrollHold.style.height = `${initScrollHold}px`; // 设置滚动内容容器的高度
setDataList(dataList); // 设置数据列表
setPosition([0, renderCount]); // 设置当前渲染的列表项的位置
}
function handleScroll() {
const { scrollTop } = scroll; // 获取滚动条距离滚动容器顶部的距离
const { defaultItemSize, renderCount } = scrollInfo;
let start = 0
let accumulatedHeight = 0
for (let i = 0; i < dataList.length; i++) {
accumulatedHeight += dataList[i].height;
if (accumulatedHeight > scrollTop) {
start = i;
break
}
}
let currentOffset = 0
for (let j = 0; j < start; j++) {
currentOffset += dataList[j].height;
}
context.style.transform = `translate3d(0, ${currentOffset}px, 0)`; // 设置滚动内容容器的偏移量
const end = Math.min(start + renderCount + 1, dataList.length)
if (end !== position[1] || start !== position[0]) { // 判断当前渲染的列表项位置是否发生改变
setPosition([start, end]); // 设置渲染列表项的起始位置和结束位置
}
}
function renderList() {
const [start, end] = position;
const renderList = dataList.slice(start, end); // 获取需要渲染的列表项
context.innerHTML = renderList
.map((item, index) => `<div class="list-item" key=${item.id} style="height: ${getRandomHeight()}px">${item.id} Item </div>`) // 渲染列表项
.join('');
// 渲染后 获取每个列表项的实际高度
const listItems = document.getElementsByClassName('list-item');
for (let i = start; i < end; i++) {
const height = listItems[i - start].offsetHeight
dataList[i].height = height;
}
updateData()
}
function updateData() {
const height= dataList.reduce((pre,cur)=> pre+cur.height,0)
scrollHold.style.height = `${height}px`; // 设置虚拟滚动内容容器的高度
}
function getRandomHeight() {
return Math.floor(Math.random() * 31) + 30
}
function setDataList(data) {
dataList = data;
}
function setPosition(pos) {
position = pos;
renderList();
}
run();
</script>
</body>
Last updated