1. The background

During development, there are always many lists displayed. When a list of orders of magnitude is rendered in a browser, it eventually degrades browser performance. If the amount of data is too large, first rendering is extremely slow, and second the page directly freezes. Of course, you can choose other ways to avoid it. Such as paging, or downloading files and so on. Here we discuss how to use virtual lists to solve this problem.

2. What are virtual lists

Simplest description: List scrolling, changesViewing areaInside the render element.



through[Estimated height of single data]To calculate the[Total list height]and[Visual area height]. And in the[Visual area height]Inside the on-demand rendering list.

3. Introduction to related concepts

The following describes some important parameters in the component information, here to understand, have an impression, the subsequent use of the time is more clear.

  • [Estimated height of single data]: Specifies the height of a specific list in the list[Fixed height]Or it could be[Dynamic height]
  • [Total list height]: tabulated when all data is rendered[Total height]
  • [Visual area height]: a container that hangs on the virtual list. The area where the list is visible
  • [Estimated number of displays]In:[Visual area height]In accordance with the[Estimated height of single data], the number of visible data items
  • [Start index]: [Visual area height]The index of the first data of the displayed data
  • [End index]: [Visual area height]The index of the last piece of data displayed
  • [Cache of position of each Item]: Because the height of the list may not be the same, the height of each data item is recorded, including index, top, bottom, and lineHeight attributes

4. Virtual list implementation

Virtual list can be simply understood as: when the list is scrolling, change the render elements in [visual area height]. According to the related concepts introduced above, we follow the following steps according to these attributes:

  • Incoming component data[Resources][Estimated height (estimatedItemSize)
  • According to the[Resources]and[Estimated height (estimatedItemSize)Calculate the initial position of each piece of data (the space for each piece of data when all rendered)
  • To calculate the[Total list height]
  • [Visual area height]Control by CSS
  • According to the[Visual area height], and calculate the number of projected displays in the visual region
  • Initializes the visual window[Header mount element]and[Tail mount element]When rolling occurs, recalculate according to the rolling difference and rolling direction[Header mount element]and[Tail mount element].

Following the introductory steps above, let’s start implementing a virtual list.

4.1 Driver development: Parameter analysis

parameter instructions type The default value
resources Source data array Array []
estimatedItemSize Estimated height of each piece of data number 32px
extrea Use to customize ItemRender, passing other parameters any none
ItemRender Each piece of data is rendered by a component React.FC const ItemRender = ({ data }: Data) => (<React.Fragment>{String(data) }</React.Fragment>)
key Generates the unique key of the item as a traversal. Fields that need to be specific to some unique value of the data for Resources. Used to improve performance. string Default order: -> ID -> Key -> Index

4.1.1 ItemRender

import React, { useState } from 'react';
import { VirtualList } from 'biz-web-library';
// Define the component to display each piece of data
const ItemRender = ({ data }) = > {
  let dindex = parseInt(data);
  let lineHeight = dindex % 2 ? '40px' : '80px';
  return (
    <div style={{ lineHeight.background: dindex % 2 ? '#f5f5f5' : '#fff' }}>
      <h3>#{dindex} title name</h3>
      <p>Write whatever you want to write, no matter how high the page is</p>
    </div>
  );
};
const ItemRenderMemo = React.memo(ItemRender);
Copy the code

4.1.2 Initializing the Data list

// Initialize the list data
const getDatas = () = > {
  const datas = [];
  for (let i = 0; i < 100000; i++) {
    datas.push(`${i} Item`);
  }
  return datas;
};
Copy the code

4.1.3 How to Use it

// Use a virtual list
export default() = > {let [resources, setResources] = useState([]);
  const changeResources = () = > {
    setResources(getDatas());
  };

  return (
    <div>
      <button onClick={changeResources}>click me </button>

      <div
        style={{
          height: '400px',
          overflow: 'auto',
          border: '1px solid #f5f5f5',
          padding: '0 10px'}} >
        <VirtualList
          ItemRender={ItemRenderMemo}
          resources={resources}
          estimatedItemSize={60}
        />
      </div>
    </div>
  );
};
Copy the code

4.2 Component initialization calculation and layout

Now that you know how to use it, let’s start implementing our component. The initialization position of each piece of data is calculated based on the data source resources passed in and the estimated height estimatedItemSize.

// The total initialization height of the loop cache list
export const initPositinoCache = (
  estimatedItemSize: number = 32,
  length: number = 0.) = > {
  let index = 0,
  positions = Array(length);
  while (index < length) {
    positions[index] = {
      index,
      height: estimatedItemSize,
      top: index * estimatedItemSize,
      bottom: (index++ + 1) * estimatedItemSize,
    };
  }
  return positions;
};
Copy the code

If the height of each item in the list is the same, then the height really doesn’t change. If the height of each piece of data is not fixed, the position will be updated as the scroll progresses. Here are some other parameters that need to be initialized:

parameter instructions type The default value
resources Source data array Array []
startOffset The offset of the visible area from the top number 0
listHeight The height of the container when all data is rendered any none
visibleCount Number of visual areas on a page number 10
startIndex Visual area starts indexing number 0
endIndex Visual area end index number 10
visibleData Visualization of the data displayed in the area Array []

In fact, for each attribute, the introduction will make its meaning clear. But the [startOffset] parameter needs to be highlighted. It is an important property that simulates infinite scrolling during scrolling. Its value, that’s how far away from the top we are as we scroll. [startOffset] combines with [visibleData] to achieve infinite scrolling.

Notice the position of positions, which correspond to external variables of a component. Remember not to hang on the static property of the component.

// Cache all item locations
let positions: Array<PositionType>;

class VirtualList extends React.PureComponent{
 
  constructor(props) {
    super(props);
    const { resources } = this.props;

    // Initialize the cache
    positions = initPositinoCache(props.estimatedItemSize, resources.length);
    this.state = {
      resources,
      startOffset: 0.listHeight: getListHeight(positions),  // The bottom attribute of the last data

      scrollRef: React.createRef(),  // virtual list container ref
      items: React.createRef(), // Virtual list displays area ref
      visibleCount: 10.// Number of viewable areas per page
      startIndex: 0.// The visual area starts indexing
      endIndex: 10.// // Visual area end index
    };
  }
  // TODO:Hide some other features...


  / / layout
  render() {
  const { ItemRender = ItemRenderComponent, extrea } = this.props;
  const { listHeight, startOffset, resources, startIndex, endIndex, items, scrollRef  } = this.state;
  let visibleData = resources.slice(startIndex, endIndex);

  return (
    <div ref={scrollRef} style={{ height:` ${listHeight}px` }}>
      <ul
        ref={items}
        style={{
          transform: `translate3d(0, ${startOffset}px.0)`,
        }}
      >
        {visibleData.map((data, index) => {
          return (
            <li key={data.id || data.key || index} data-index={` ${startIndex + index} `} >
              <ItemRender data={data} {. extrea} / >
            </li>
          );
        })}
      </ul>
    </div>); }}Copy the code

4.3 Rolling trigger registration events and updates

Register onScroll to the DOM with [componentDidMount]. In a scrolling event, use requestAnimationFrame, which takes advantage of the browser’s free time to execute and can improve code performance. If you want to understand more, check out the API in action.


componentDidMount() {
  events.on(this.getEl(), 'scroll'.this.onScroll, false);
  events.on(this.getEl(), 'mousewheel', NOOP, false);

  // Calculate the latest node based on the render
  let visibleCount = Math.ceil(this.getEl().offsetHeight / estimatedItemSize);
  if (visibleCount === this.state.visibleCount || visibleCount === 0) {
    return;
  }
  // Update endIndex, listHeight/ offset as visibleCount changes
  this.updateState({ visibleCount, startIndex: this.state.startIndex });
}

getEl = () = > {
    let el = this.state.scrollRef || this.state.items;
    letparentEl: any = el.current? .parentElement;switch (window.getComputedStyle(parentEl)? .overflowY) {case 'auto':
      case 'scroll':
      case 'overlay':
      case 'visible':
        return parentEl;
    }
    return document.body;
};

onScroll = () = > {
    requestAnimationFrame(() = > {
      let { scrollTop } = this.getEl();
      let startIndex = binarySearch(positions, scrollTop);

      // Update endIndex, listHeight/ offset as startIndex changes
      this.updateState({ visibleCount: this.state.visibleCount, startIndex});
    });
  };
Copy the code

Let’s take a look at the key steps. When scrolling, we can get the [scrollTop] of the current [scrollRef] virtual list container, and the startIndex for that position through the distance and the [positions] that record all position attributes for each item. Here, in order to improve performance, we find through dichotomy:

// Add the tool to the tool file
export const binarySearch = (list: Array<PositionType>, value: number = 0) = > {
  let start: number = 0;
  let end: number = list.length - 1;
  let tempIndex = null;
  while (start <= end) {
    let midIndex = Math.floor((start + end) / 2);
    let midValue = list[midIndex].bottom;

    // if the value is equal, the startIndex is returned directly to the node found.
    if (midValue === value) {
      return midIndex + 1;
    }
    // If the intermediate value is less than the incoming value, the node corresponding to value is larger than start, and start is moved back one bit
    else if (midValue < value) {
      start = midIndex + 1;
    }
    // If the intermediate value is greater than the incoming value, end moves to mid-1 before the intermediate value
    else if (midValue > value) {
      // tempIndex stores all nearest values with value
      if (tempIndex === null || tempIndex > midIndex) {
        tempIndex = midIndex;
      }
      end = midIndex - 1; }}return tempIndex;
};
Copy the code

Once we get startIndex, we update the values of all the properties in the component State based on startIndex.

 updateState = ({ visibleCount, startIndex }) = > {
    // Update the data according to the newly calculated node
    this.setState({
      startOffset: startIndex >= 1 ? positions[startIndex - 1]? .bottom :0.listHeight: getListHeight(positions),
      startIndex,
      visibleCount,
      endIndex: getEndIndex(this.state.resources, startIndex, visibleCount)
    });
  };

// Here are the utility functions, in other files
export const getListHeight = (positions: Array<PositionType>) = > {
    let index = positions.length - 1;
    return index < 0 ? 0 : positions[index].bottom;
  };

export const getEndIndex = (
  resources: Array<Data>,
  startIndex: number,
  visibleCount: number,
) = > {
  let resourcesLength = resources.length;
  let endIndex = startIndex + visibleCount;
  return resourcesLength > 0 ? Math.min(resourcesLength, endIndex) : endIndex;
}
Copy the code

4.4 Item height unequal update

At this point, we’re done with the basic DOM scrolling, data updating, and other logic. However, in the testing process, it will be found that if the height is different, the operation such as position update has not been carried out? Where do I put these? Here’s where our [componentDidUpdate] comes in. Each time the DOM completes rendering, the height of the displayed item should be updated to the [position] property. The current total height [istHeight] and offset [startOffset] must be updated at the same time.

 componentDidUpdate() {
  this.updateHeight();
}

  
updateHeight = () = > {
  let items: HTMLCollection = this.state.items.current? .children;if(! items.length)return;

  // Update the cache
  updateItemSize(positions, items);

  // Update the total height
  let listHeight = getListHeight(positions);

  // Update the total offset
  let startOffset = getStartOffset(this.state.startIndex, positions);

  this.setState({
    listHeight,
    startOffset,
  });
};

// Here are the utility functions, in other files
export const updateItemSize = (
  positions: Array<PositionType>,
  items: HTMLCollection,
) = > {
  Array.from(items).forEach(item= > {
    let index = Number(item.getAttribute('data-index'));
    let { height } = item.getBoundingClientRect();
    let oldHeight = positions[index].height;

    // There is a difference, update all nodes after this node
    let dValue = oldHeight - height;
    if (dValue) {
      positions[index].bottom = positions[index].bottom - dValue;
      positions[index].height = height;

      for (let k = index + 1; k < positions.length; k++) {
        positions[k].top = positions[k - 1].bottom; positions[k].bottom = positions[k].bottom - dValue; }}}); };// Get the current offset
export const getStartOffset = (
  startIndex: number,
  positions: Array<PositionType> = [],
) = > {
  return startIndex >= 1 ? positions[startIndex - 1]? .bottom :0;
};

export const getListHeight = (positions: Array<PositionType>) = > {
  let index = positions.length - 1;
  return index < 0 ? 0 : positions[index].bottom;
};
Copy the code

4.5 Updated component data when External Parameter Data Is Changed

As a final step, if the external data source we passed in, etc., changes, then we need to synchronize the data. This operation is of course delivered in the getDerivedStateFromProps method.

 static getDerivedStateFromProps(nextProps: VirtualListProps, prevState: VirtualListState,) {
    const { resources, estimatedItemSize } = nextProps;
    if(resources ! == prevState.resources) { positions = initPositinoCache(estimatedItemSize, resources.length);// Update the height
      let listHeight = getListHeight(positions);

      // Update the total offset
      let startOffset = getStartOffset(prevState.startIndex, positions);

     
      let endIndex = getEndIndex(resources, prevState.startIndex, prevState.visibleCount);
     
      return {
        resources,
        listHeight,
        startOffset,
        endIndex,
      };
    }
    return null;
  }
Copy the code

5 conclusion

A complete Vitural List component is complete, which is customized for each ItemRender function, so as long as it is in the form of a list, you can virtually scroll whoever you want. Of course, according to the information on the Internet, the picture of the relevant scroll, because of network problems, can not ensure the access to the list item is really high, which may cause inaccurate situation. Here is no discussion, interested partners can further.