This is the 8th day of my participation in the First Challenge 2022. For details: First Challenge 2022.
Why use virtual lists
scenario
Let’s take a look at this. This list item has 6,000 entries, and the page has four such lists, page load and scroll lists, which are visible to the naked eye. (Tip: The frame rate is low, so you can’t see it clearly)
What’s the reason?
This is because there are too many DOM elements in the page, causing the browser to render slowly when the page is initialized and scrolling through the list.
What a virtual list is
There were 6000 pieces of data to render, but only 7 pieces were visible in the container box. The virtual list is the maximum number of items that can be contained in the visual area of 7 out of 6,000 data items, meaning that there are only 7 real DOM list elements in the page. Then monitor the scrolling of the container to update the seven pieces of data in real time.
Version 1: The height of the list item is fixed
rendering
As the list scrolls, it feels like dove, but when you look at the DOM tree, there are only 10 pieces of data. This is based on the height of the visible area and the height of each term.
The dom tree structure
-
A Container box that contains a ListBox inside which to render each list item.
-
Listbox height = Height of each item * Total number of list items. This causes the container to be stretched open, creating scroll bars
-
Initialization calculates the number of items that need to be displayed in the visual area, as well as the start index, the end index, etc.
- Be sure to use math.ceil () instead of floor() when counting bars
-
Listen for a Container box scroll event that evaluates the start and end indexes as it scrolls.
-
This makes the list seamless
In the code
import React, { memo, useState, useMemo, useCallback, useRef } from "react";
import styled from "styled-components";
const Container = styled.div`
overflow-y: auto;
overflow-x: hidden;
height: ${({ height }) => height};
`
const ListBox = styled.div` background-color: pink; position: relative; `
const VirList3 = memo(function ({ list = [], containerHeight = 800, ItemBox = <></>, itemHeight = 50. props }) {
const ContainerRef = useRef();
const [startIndex, setStartIndex] = useState(0);
// It is used to open the Container and calculate its height
const wraperHeight = useMemo(function () {
return list.length * itemHeight;
}, [list, itemHeight])
// The maximum number of bars to display in the viewable area
const limit = useMemo(function () {
return Math.ceil(containerHeight / itemHeight);
}, [startIndex]);
// The end index of the list displayed in the current viewable area
const endIndex = useMemo(function () {
return Math.min(startIndex + limit, list.length - 1);
}, [startIndex, limit]);
const handleSrcoll = useCallback(function (e) {
if(e.target ! == ContainerRef.current)return;
const scrollTop = e.target.scrollTop;
let currentIndex = Math.floor(scrollTop / itemHeight);
if(currentIndex ! == startIndex) { setStartIndex(currentIndex); } }, [ContainerRef, itemHeight, startIndex])const renderList = useCallback(function () {
const rows = [];
for (let i = startIndex; i <= endIndex; i++) {
// Render each list item
rows.push(<ItemBox
data={i}
key={i}
style={{
width: "100% ",height: itemHeight - 1+"px",
borderBottom: "1px solid #aaa",
position: "absolute",
top: i * itemHeight+"px",
left: 0.right: 0,
}} />)}return rows;
}, [startIndex, endIndex, ItemBox])
return (<Container
height={containerHeight+"px"}
ref={ContainerRef}
onScroll={handleSrcoll}>
<ListBox
style={{ height: wraperHeight+"px}} ">
{renderList()}
</ListBox>
</Container>)})export default VirList3;
Copy the code
Using the component
Version 2: The list item height is not fixed
The problem
How do you calculate the number of items that should be displayed in the current viewable area if the height of the list item is not fixed, and how do you change the first index as you scroll to make it seamless?
Dom structure
Add a div wrap list item: this item refers to the Wraper box
The overall train of thought
-
The height of list items is not fixed, so variables such as limit cannot be calculated. So we pre-define a default list item height that needs to be appropriate for your project.
-
Use a cache array to store the location of each list item. Each object contains an index, the distance from the top of each item to the ListBox container, the distance from the bottom of each item to the ListBox container, and the height of each item. Use useState to initialize the cache array.
-
Calculate the limit: Because the height of each item is not fixed, it needs to be calculated in real time according to the container roll. UseMemo is used as a calculation property, relying on the cache array for real-time updates.
-
The default ListBox height is the number of items multiplied by the default ListBox height, which is recalculated when the cache array is updated. The code is at wraperHeight.
-
GetTransform value: Adjust the height of the Wraper box while scrolling to achieve seamless scrolling.
-
While scrolling, recalculate the start index (using binary lookup), end index, and limit.
-
As the page scrolls, the cache array retrieves the custom attribute data-id from the list item to get the current item index, and then calculates the actual location of the current item. Tread pit tip: Note that data-id is used here, not the index of the current loop.
Binary search
In the code
import React, { memo, useState, useMemo, useCallback, useRef, useEffect } from "react";
import styled from "styled-components";
const Container = styled.div`
overflow-y: auto;
height: ${({ height }) => height};
`
const ListBox = styled.div` background-color: pink; position: relative; `
const Wraper = styled.div` `
const VirList4 = memo(function ({
list = [],
containerHeight = 800,
ItemBox = <></>,
estimatedItemHeight = 90. props }) {
const ContainerRef = useRef();
const WraperRef = useRef();
const [startIndex, setStartIndex] = useState(0);
const [scrollTop, setScrollTop] = useState(0);
const [positionCache, setPositionCache] = useState(function () {
const positList = [];
list.forEach((_, i) = > {
positList[i] = {
index: i,
height: estimatedItemHeight,
top: i * estimatedItemHeight,
bottom: (i + 1) * estimatedItemHeight,
}
})
return positList;
})
const limit = useMemo(function () {
let sum = 0
let i = 0
for (; i < positionCache.length; i++) {
sum += positionCache[i].height;
if (sum >= containerHeight) {
break}}return i;
}, [positionCache]);
const endIndex = useMemo(function () {
return Math.min(startIndex + limit, list.length - 1);
}, [startIndex, limit]);
const wraperHeight = useMemo(function () {
let len = positionCache.length;
if(len ! = =0) {
return positionCache[len - 1].bottom
}
return list.length * estimatedItemHeight;
}, [list, positionCache, estimatedItemHeight])
useEffect(function () {
const nodeList = WraperRef.current.childNodes;
const positList = [...positionCache]
let needUpdate = false;
nodeList.forEach((node, i) = > {
let newHeight = node.getBoundingClientRect().height;
const nodeID = Number(node.id.split("-") [1]);
const oldHeight = positionCache[nodeID]["height"];
const dValue = oldHeight - newHeight;
if (dValue) {
needUpdate = true;
positList[nodeID].height = node.getBoundingClientRect().height;
positList[nodeID].bottom = nodeID > 0 ? (positList[nodeID - 1].bottom + positList[nodeID].height) : positList[nodeID].height;
positList[nodeID].top = nodeID > 0 ? positList[nodeID - 1].bottom : 0; }})if (needUpdate) {
setPositionCache(positList)
}
}, [scrollTop])
const getTransform = useCallback(function () {
return `translate3d(0,${startIndex >= 1 ? positionCache[startIndex - 1].bottom : 0}px,0)`
}, [positionCache, startIndex]);
const handleSrcoll = useCallback(function (e) {
if(e.target ! == ContainerRef.current)return;
const scrollTop = e.target.scrollTop;
setScrollTop(scrollTop)
const currentStartIndex = getStartIndex(scrollTop);
console.log(currentStartIndex);
if(currentStartIndex ! == startIndex) { setStartIndex(currentStartIndex);console.log(startIndex + "= = = =" + limit + "-- = = = =" + endIndex)
}
}, [ContainerRef, estimatedItemHeight, startIndex])
const renderList = useCallback(function () {
const rows = [];
for (let i = startIndex; i <= endIndex; i++) {
rows.push(<ItemBox
data={list[i]}
index={i}
key={i}
style={{
width: "100% ",borderBottom: "1px solid #aaa,}} "/ >)}return rows;
}, [startIndex, endIndex, ItemBox])
return (<Container
height={containerHeight+"px"}
ref={ContainerRef}
onScroll={handleSrcoll}>
<ListBox
style={{ height: wraperHeight+"px}} ">
<Wraper
style={{
transform: getTransform()}}ref={WraperRef}
>
{renderList()}
</Wraper>
</ListBox>
</Container>)})export default VirList4;
Copy the code
use
rendering