Skip to main content

虚拟列表

React 实现虚拟列表

原理: 假设子项目固定高度 100px,一共 100 项 先计算出总高度 10000px 先渲染一个空的元素 list-phantom 高度为 10000px

然后是真正的列表,如果我们一次渲染 10 个 则 list 高度为 1000px

滚动时计算 scrollTop,通过 scrollTop 计算当前视口最顶部的元素索引 start

我们只往下滚动 0.5 项时,我们只想让第一项往上移动 0.5 项目,而不是直接触发 listData 内首项得到更新

// 通过 scrollTop % itemSize 计算出剩余的那些未形成一项的移动 // 用 scrollTop 减就是该往上的偏移量

计算 startOffset(distance 为 50px) 150px - 150px%100px = 100px 即在滚动 150px 的基础上减少滚动 50px 只滚动 100px

import { forwardRef, useState } from "react";
import { flushSync } from "react-dom";

// 动态列表组件
const VariableSizeList = forwardRef(
({ containerHeight, getItemHeight, itemCount, itemData, children }, ref) => {
ref.current = {
resetHeight: () => {
setOffsets(genOffsets());
},
}; // children 语义不好,赋值给 Component

const Component = children;
const [scrollTop, setScrollTop] = useState(0); // 滚动位置 // 根据 getItemHeight 生成 offsets // 本质是前缀和
const genOffsets = () => {
const a = [];
a[0] = getItemHeight(0);
// 生成 [0,20,40,60]
for (let i = 1; i < itemCount; i++) {
a[i] = getItemHeight(i) + a[i - 1];
}
return a;
}; // 所有 items 的位置

const [offsets, setOffsets] = useState(() => {
return genOffsets();
}); // 找 startIdx 和 endIdx // 这里用了普通的查找,更好的方式是二分查找

let startIdx = offsets.findIndex((pos) => pos > scrollTop);
let endIdx = offsets.findIndex((pos) => pos > scrollTop + containerHeight);
if (endIdx === -1) endIdx = itemCount;

const paddingCount = 2;
startIdx = Math.max(startIdx - paddingCount, 0); // 处理越界情况
endIdx = Math.min(endIdx + paddingCount, itemCount - 1); // 计算内容总高度

const contentHeight = offsets[offsets.length - 1]; // 需要渲染的 items

const items = [];
for (let i = startIdx; i <= endIdx; i++) {
const top = i === 0 ? 0 : offsets[i - 1];
const height = i === 0 ? offsets[0] : offsets[i] - offsets[i - 1];
items.push(
<Component
key={i}
index={i}
style={{
position: "absolute",
left: 0,
top,
width: "100%",
height,
}}
data={itemData}
/>
);
}

return (
<div
style={{
height: containerHeight,
overflow: "auto",
position: "relative",
}}
onScroll={(e) => {
// 不用useEffevt 触发渲染
flushSync(() => {
setScrollTop(e.target.scrollTop);
});
}}
>
        <div style={{ height: contentHeight }}>{items}</div>
      
</div>
);
}
);

vue

    scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引
this.start = Math.floor(scrollTop / this.itemSize);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.startOffset = scrollTop - (scrollTop % this.itemSize);
}


//偏移量对应的style
getTransform(){
return `translate3d(0,${this.startOffset}px,0)`;
},



// 计算属性实时计算需要的项目
visibleData(){
return this.listData.slice(this.start, Math.min(this.end,this.listData.length));
}