1. Background During the development process, we always encounter the display of many lists. When lists of this magnitude are rendered to the browser, it will eventually cause the browser's performance to degrade. If the amount of data is too large, first of all, the rendering will be extremely slow, and secondly, the page will be stuck directly. Of course, you can choose other ways to avoid it. For example, paging, or downloading files and so on. Here we discuss how to use virtual lists to solve this problem. 2. What is a virtual listThe simplest description: When the list scrolls, change the rendering elements in the visible area. The [total height of the list] and [height of the visualization area] are calculated through the [estimated height of a single data item]. And render the list as needed within [visualization area height]. 3. Introduction to related conceptsThe following introduces some very important parameter information in the component. Let's understand it here first to have an impression, so that it will be clearer when you use it later.
4. Virtual list implementationThe virtual list can be simply understood as: when the list is scrolled, the rendering elements within the [visualization area height] are changed. According to the relevant concepts introduced above, we follow these steps based on these properties:
According to the above introduction steps, let's start to implement a virtual list. 4.1 Driver Development: Parameter Analysis
4.1.1 ItemRender import React, { useState } from 'react'; import { VirtualList } from 'biz-web-library'; // Define the component for displaying 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, no limit on page height</p> </div> ); }; const ItemRenderMemo = React.memo(ItemRender); 4.1.2 Data List Initialization // Initialize list data const getDatas = () => { const datas = []; for (let i = 0; i < 100000; i++) { datas.push(`${i} Item`); } return datas; }; 4.1.3 How to use // Using 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> ); }; 4.2 Component initialization calculation and layoutNow that we know how to use it, let's start implementing our component. According to the passed in data source resources and estimated height estimatedItemSize, calculate the initialization position of each piece of data. // Overall initialization height of the circular 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; }; If the height of each data in the list is consistent, then this height will not change. If the height of each piece of data is not fixed, the position will be updated during the scrolling process. Here are some other parameters that need to be initialized:
In fact, for each attribute, its significance can be clearly seen after a brief introduction. However, the [startOffset] parameter needs to be introduced in detail. It is an important property that simulates infinite scrolling during the scrolling process. Its value indicates the position from the top during our scrolling process. [startOffset] achieves the effect of infinite scrolling by combining [visibleData]. // Cache the positions of all items let positions: Array<PositionType>; class VirtualList extends React.PureComponent{ constructor(props) { super(props); const { resources } = this.props; // Initialize cache positions = initPositinoCache(props.estimatedItemSize, resources.length); this.state = { resources, startOffset: 0, listHeight: getListHeight(positions), // bottom attribute of the last data in positions scrollRef: React.createRef(), // virtual list container ref items: React.createRef(), // Virtual list display area ref visibleCount: 10, // Number of visible areas on a page startIndex: 0, // Start index of visible area endIndex: 10, // // End index of visible area }; } // TODO: Hide some other functionality. . . . . //Layout render() { const { ItemRender = ItemRenderComponent, extension } = 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> ); } } 4.3 Scrolling triggers registration events and updatesRegister onScroll to DOM through [componentDidMount]. In the scrolling event, requestAnimationFrame is used. This method uses the browser's idle time to execute, which can improve the performance of the code. If you want to have a deeper understanding, you can check the specific use of this API. componentDidMount() { events.on(this.getEl(), 'scroll', this.onScroll, false); events.on(this.getEl(), 'mousewheel', NOOP, false); // Calculate the latest node based on rendering let visibleCount = Math.ceil(this.getEl().offsetHeight / estimatedItemSize); if (visibleCount === this.state.visibleCount || visibleCount === 0) { return; } // Update endIndex, listHeight/offset because visibleCount changed this.updateState({ visibleCount, startIndex: this.state.startIndex }); } getEl = () => { let el = this.state.scrollRef || this.state.items; let parentEl: 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); // Because startIndex changes, update endIndex, listHeight/offset this.updateState({ visibleCount: this.state.visibleCount, startIndex}); }); }; Next, we analyze the key steps. When scrolling, we can get the [scrollTop] of the current [scrollRef] virtual list container. Through this distance and [positions] (which records all the position properties of each item), we can get the startIndex of that position. To improve performance, we use binary search: // Tool function, put into 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 values are equal, the found node is returned directly (because it is bottom, startIndex should be the next node) if (midValue === value) { return midIndex + 1; } // If the middle value is less than the input value, it means that the node corresponding to value is greater than start, and start moves back one position else if (midValue < value) { start = midIndex + 1; } // If the middle value is greater than the input value, it means that value is before the middle value, and the end node moves to mid - 1 else if (midValue > value) { // tempIndex stores all the values closest to value if (tempIndex === null || tempIndex > midIndex) { tempIndex = midIndex; } end = midIndex - 1; } } return tempIndex; }; Once we get the startIndex, we will update the values of all the properties in the component State based on the startIndex. updateState = ({ visibleCount, startIndex }) => { // Update 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) }); }; // The following is a tool function, placed 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; } 4.4 Update item heights when they are not equal At this point, we have completed the basic DOM scrolling, data update and other logic. But during the test, you will find that if the height is not equal, the position and other operations have not been updated? Where to put these? componentDidUpdate() { this.updateHeight(); } updateHeight = () => { let items: HTMLCollection = this.state.items.current?.children; if (!items.length) return; // Update cache updateItemSize(positions, items); // Update the total height let listHeight = getListHeight(positions); // Update total offset let startOffset = getStartOffset(this.state.startIndex, positions); this.setState({ listHeight, startOffset, }); }; // The following is a tool function, placed 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; //If 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; }; 4.5 External parameter data changes, component data updates At this last step, if the external data source we passed in has changed, we have to synchronize the data. This operation is of course completed in the getDerivedStateFromProps method. static getDerivedStateFromProps( nextProps: VirtualListProps, prevState: VirtualListState, ) { const { resources, estimatedItemSize } = nextProps; if (resources !== prevState.resources) { positions = initPositinoCache(estimatedItemSize, resources.length); // Update height let listHeight = getListHeight(positions); // Update total offset let startOffset = getStartOffset(prevState.startIndex, positions); let endIndex = getEndIndex(resources, prevState.startIndex, prevState.visibleCount); return { resources, listHeight, startOffset, endIndex, }; } return null; } 5 Conclusion OK, a complete vitural list component is completed. Because the render function of each data ItemRender is customized, you can scroll virtually any item as long as it is in list form. Of course, according to the information I have read online, due to network problems, the scrolling of pictures cannot guarantee the true height of the list items, which may cause inaccuracies. We will not discuss this here for now, and those who are interested can go into more depth. This is the end of this article about the implementation of React virtual list. For more relevant React virtual list content, please search 123WORDPRESS.COM's previous articles or continue to browse the following related articles. I hope everyone will support 123WORDPRESS.COM in the future! You may also be interested in:
|
<<: MySQL 8.0.12 installation and configuration method graphic tutorial (windows10)
>>: VMware virtualization kvm installation and deployment tutorial summary
1. Implementation principle of Nginx load balanci...
Table of contents 1. Template 2. Generics 3. Gene...
Requirement: Celery is introduced in Django. When...
1. Overview MySQL version: 5.6.21 Download addres...
In HTML, the <img> tag is used to define an...
About a year ago, I wrote an article: Analysis of...
HTML meta viewport attribute description What is ...
When using TensorFlow for deep learning, insuffic...
The idea of using token for login verification ...
Introduction Closure is a very powerful feature i...
The following error occurred while installing the...
Core code /*-------------------------------- Find...
No matter how wonderful your personal website is,...
This article records some major setting changes w...
At first I thought it was a speed issue, so I late...