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 area
Inside 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.