本节,咱们实现一个极简版的虚构列表,固定尺寸的虚构列表,麻雀虽小,却是五脏俱全哦!
需要
实现一个固定尺寸的虚构渲染列表组件,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, ]; }}// 计算区间开始索引,滚动间隔 除以 每个单元尺寸 就是 startIndexexport 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, ]; }}