In this section, we achieve a minimalist version of the virtual list, a fixed size of the virtual list, sparrow is small, but it is all five viscera!

demand

Implement a fixed-size virtual rendering list component with the props properties as follows:

props: { 
    width: number; 
    height: number;
    itemCount: number;
    itemSize: number;
}

Usage:

const Row = (.. args) => (<div className="Row"></div>); <List className={"List"} width={300} height={300} itemCount={10000} itemSize={40}> {Row} </List>

implementation

Any technology stack can be used. The React project is used here, so choose React to implement.

Initialize the project

Initialize an application with create-react-app, then launch it, and clean up the demo code.

Virtual list

According to the analysis in the last section, our core technology implementation is a render rendering function, used to render data; An onScroll function listens for scrolling events, updates the data range [startIndex, endIndex], and then rerenders. The approximate pseudocode is as follows:

class List extends React.PureComponent {
  state = {};
  render() {};
  onScroll() {};
}

Next, we fill in the details. First of all, we need to render the DOM initialization of the first screen according to the data, that is, to realize the render function logic first, we use the absolute positioning method for DOM typesetting.

Render () {props const {children, width, height, ItemCount, layout, ItemKey = defaultItemKey,} = this.props; props; // Const horizontal = layout == "horizontal"; Const [startIndex, stopIndex] = this._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), // help calculate the DOM position style})); 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, here the render function logic is written, is not super simple. Next we implement the helper functions used in the following render function.

getEstimatedTotalSize

First look at the implementation of getEstimatedTotalSize to calculate the total size of the function:

Export const getEstimatedTotalSize = ({itemCount, itemSize}) => itemSize * itemCount;

_getRangeToRender

Calculate the need to render the data interval function implementation

_getRangeToRender() {// OverscanCount is the number of buffer, default is 1 const {itemCount, overscanCount = 1} = this.props; // The initial default is 0 const {scrolloffSet} = this.state; if (itemCount === 0) { return [0, 0, 0, 0]; } function const startIndex = getStartIndexForOffset(this.props, scrolloffSet,); / / auxiliary function, according to the end of the start index to calculate the interval index const stopIndex = getStopIndexForStartIndex (enclosing props, startIndex, scrollOffset,); return [ Math.max(0, startIndex - overscanCount), Math.max(0, Math.min(itemCount - 1, stopIndex + overscanCount)), startIndex, stopIndex, ]; }} // Calculate interval start index, StartIndex export const getStartIndexForOffset = ({itemCount, itemSize}, offset) = bb0 Math.max(0, itemCount, itemSize) Math.min(itemCount - 1, Math.floor(offset / itemSize))); // Calculate interval end index, Export index + visible area size/itemSize start 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 ) ); };

Calculates the element position _getItemStyle

Position is calculated according to Index * ItemSize

_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; };

Well, at this point, all the logic of the render function has been implemented.

Listen for the scrolling onScroll implementation

And finally, we just listen on the onScroll event, update the index interval, and we’re done

// simple, just a setState operation, _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, }; }); };

The complete code

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"; // monitor the scrolling function 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, ]; }}