本节,咱们实现一个极简版的虚构列表,固定尺寸的虚构列表,麻雀虽小,却是五脏俱全哦!
需要
实现一个固定尺寸的虚构渲染列表组件,props 属性如下:
props: {
width: number;
height: number;
itemCount: number;
itemSize: number;
}
应用形式:
const Row = (..args) => (<div className="Row"></div>);
<List className={"List"} width={300} height={300} itemCount={10000} itemSize={40}>
{Row}
</List>
实现
什么技术栈都能够,这里我的项目应用的 react,那就选用 react 来实现。
初始化我的项目
应用 create-react-app 初始化一个利用,而后启动,清理掉 demo 的代码。
虚构列表
依据上一节的剖析,咱们核心技术实现是 一个 render 渲染函数,用来渲染数据;一个 onScroll 函数监听滚动事件,去更新 数据区间 [startIndex, endIndex],而后从新 render。大略伪代码如下:
class List extends React.PureComponent {state = {};
render() {};
onScroll() {};
}
接下来咱们进行细节填充实现,首先咱们须要依据数据渲染出第一屏初始化的 dom,即要先实现 render 函数逻辑,咱们采纳相对定位的形式进行 dom 排版。
render() {
// 从 props 解析属性
const {
children,
width,
height,
itemCount,
layout,
itemKey = defaultItemKey,
} = this.props;
// 预留方向设定属性
const isHorizontal = layout === "horizontal";
// 假如有一个函数_getRangeToRender 能够帮咱们计算出 渲染区间
const [startIndex, stopIndex] = this._getRangeToRender();
const items = [];
if (itemCount > 0) {
// 循环创立元素
for (let index = startIndex; index <= stopIndex; index++) {
items.push(
createElement(children, {data: {},
key: itemKey(index),
index,
style: this._getItemStyle(index), // 帮忙计算 dom 的地位款式
})
);
}
}
// 假如 getEstimatedTotalSize 函数能够帮忙咱们计算出总尺寸
const estimatedTotalSize = getEstimatedTotalSize(this.props,);
return createElement(
"div",
{
onScroll: this.onScroll,
style: {
position: "relative",
height,
width,
overflow: "auto",
WebkitOverflowScrolling: "touch",
willChange: "transform",
},
},
createElement("div", {
children: items,
style: {
height: isHorizontal ? "100%" : estimatedTotalSize,
pointerEvents: "none",
width: isHorizontal ? estimatedTotalSize : "100%",
},
})
);
}
OK,到了这里 render 函数的逻辑就写完了,是不是超级简略。接下来咱们实现以下 render 函数外面应用到的辅助函数.
getEstimatedTotalSize
先看 getEstimatedTotalSize 计算总尺寸函数的实现:
// 总尺寸 = 总个数 * 每个 size
export const getEstimatedTotalSize = ({itemCount, itemSize}) =>
itemSize * itemCount;
_getRangeToRender
计算须要渲染的数据区间函数实现
_getRangeToRender() {
// overscanCount 是缓冲区的数量,默认设置 1
const {itemCount, overscanCount = 1} = this.props;
// 曾经滚动的间隔,初始默认 0
const {scrollOffset} = this.state;
if (itemCount === 0) {return [0, 0, 0, 0];
}
// 辅助函数,依据 滚动间隔计算出 区间开始的索引
const startIndex = getStartIndexForOffset(
this.props,
scrollOffset,
);
// 辅助函数,依据 区间开始的索引计算出 区间完结的索引
const stopIndex = getStopIndexForStartIndex(
this.props,
startIndex,
scrollOffset,
);
return [Math.max(0, startIndex - overscanCount),
Math.max(0, Math.min(itemCount - 1, stopIndex + overscanCount)),
startIndex,
stopIndex,
];
}
}
// 计算区间开始索引,滚动间隔 除以 每个单元尺寸 就是 startIndex
export const getStartIndexForOffset = ({itemCount, itemSize}, offset) =>
Math.max(0, Math.min(itemCount - 1, Math.floor(offset / itemSize)));
// 计算区间完结索引,开始索引 + 可见区域 size / itemSize 即可
export const getStopIndexForStartIndex = ({ height, itemCount, itemSize, layout, width},
startIndex,
scrollOffset
) => {
const isHorizontal = layout === "horizontal";
const offset = startIndex * itemSize;
const size = isHorizontal ? width : height;
const numVisibleItems = Math.ceil((size + scrollOffset - offset) / itemSize);
return Math.max(
0,
Math.min(
itemCount - 1,
startIndex + numVisibleItems - 1
)
);
};
计算元素地位 _getItemStyle
计算形式:依据 index * itemSize 即可计算出 position
_getItemStyle = (index) => {const { layout} = this.props;
let style;
const offset = index * itemSize;
const size = itemSize;
const isHorizontal = layout === "horizontal";
const offsetHorizontal = isHorizontal ? offset : 0;
style = {
position: "absolute",
left: offsetHorizontal,
top: !isHorizontal ? offset : 0,
height: !isHorizontal ? size : "100%",
width: isHorizontal ? size : "100%",
};
return style;
};
好了,到此地位,render 函数的所有逻辑全副实现结束了。
监听滚动 onScroll 实现
最初一步,只须要监听 onScroll 事件,更新 数据索引区间,咱们的性能就欠缺了
// 非常简单,只是一个 setState 操作,更新滚动间隔即可
_onScrollVertical = (event) => {const { clientHeight, scrollHeight, scrollTop} = event.currentTarget;
this.setState((prevState) => {if (prevState.scrollOffset === scrollTop) {return null;}
const scrollOffset = Math.max(
0,
Math.min(scrollTop, scrollHeight - clientHeight)
);
return {scrollOffset,};
});
};
残缺代码
class List extends PureComponent {
_outerRef;
static defaultProps = {
layout: "vertical",
overscanCount: 2,
};
state = {
instance: this,
scrollDirection: "forward",
scrollOffset: 0,
};
render() {
const {
children,
width,
height,
itemCount,
layout,
itemKey = defaultItemKey,
} = this.props;
const isHorizontal = layout === "horizontal";
// 监听滚动函数
const onScroll = isHorizontal
? this._onScrollHorizontal
: this._onScrollVertical;
const [startIndex, stopIndex] = this._getRangeToRender();
const items = [];
if (itemCount > 0) {for (let index = startIndex; index <= stopIndex; index++) {
items.push(
createElement(children, {data: {},
key: itemKey(index),
index,
style: this._getItemStyle(index),
})
);
}
}
const estimatedTotalSize = getEstimatedTotalSize(this.props);
return createElement(
"div",
{
onScroll,
style: {
position: "relative",
height,
width,
overflow: "auto",
WebkitOverflowScrolling: "touch",
willChange: "transform",
},
},
createElement("div", {
children: items,
style: {
height: isHorizontal ? "100%" : estimatedTotalSize,
pointerEvents: "none",
width: isHorizontal ? estimatedTotalSize : "100%",
},
})
);
}
_onScrollHorizontal = (event) => {};
_onScrollVertical = (event) => {const { clientHeight, scrollHeight, scrollTop} = event.currentTarget;
this.setState((prevState) => {if (prevState.scrollOffset === scrollTop) {return null;}
const scrollOffset = Math.max(
0,
Math.min(scrollTop, scrollHeight - clientHeight)
);
return {scrollOffset,};
});
};
_getItemStyle = (index) => {const { layout} = this.props;
let style;
const offset = getItemOffset(this.props, index, this._instanceProps);
const size = getItemSize(this.props, index, this._instanceProps);
const isHorizontal = layout === "horizontal";
const offsetHorizontal = isHorizontal ? offset : 0;
style = {
position: "absolute",
left: offsetHorizontal,
top: !isHorizontal ? offset : 0,
height: !isHorizontal ? size : "100%",
width: isHorizontal ? size : "100%",
};
return style;
};
// 计算出须要渲染的数据索引区间
_getRangeToRender() {const { itemCount, overscanCount = 1} = this.props;
const {scrollOffset} = this.state;
if (itemCount === 0) {return [0, 0, 0, 0];
}
const startIndex = getStartIndexForOffset(
this.props,
scrollOffset
);
const stopIndex = getStopIndexForStartIndex(
this.props,
startIndex,
scrollOffset
);
return [Math.max(0, startIndex - overscanCount),
Math.max(0, Math.min(itemCount - 1, stopIndex + overscanCount)),
startIndex,
stopIndex,
];
}
}