current position:Home>About virtual lists
About virtual lists
2022-04-29 16:28:24【Diao Min hero】
Preface
At work , We may encounter the scene of rendering big data list . The first thing we think of is the ready-made list of virtual plug-ins , however , There are so many operational requirements for the product , Online virtual list plug-ins sometimes can only do nothing !. This article first introduces the general virtual list ( Component internal scrolling ) How to implement , Then on this basis, it expounds the idea of realizing virtual list by listening to the scrolling of external elements of components .
principle
Render some data , When scrolling, calculate what data needs to be rendered in real time .
problem
1. Since only part of the data is rendered , So how do we open the scroll bar ?
2. How the rendered data should be laid out ?
3. At what point in time to calculate what data needs to be rendered ?
Generate a simple list
First we need to have a div
, It's our container element , We set a fixed height for it height
, This height is transmitted from the outside of the component , When the content height is higher than height
when , The container needs to be rolled , So you need to set up some css style ,overflow:auto
.
<div
className="virtualList"
style={{
height: `${height}px`,
}}
onScroll={onListScroll}
ref={scrollRef as LegacyRef<HTMLDivElement>}
>
</div>
// css part
.virtualList{
position: relative;
width: 100%;
overflow: auto;
}
Copy code
When we have such a container , We also need to add some scrolling events to it ,onScroll={onListScroll}
,
const onListScroll=(e: UIEvent<HTMLDivElement>) => {
const target = e.target as any;
const { scrollTop } = target;
console.log(scrollTop);
}
Copy code
So we can get the height of the scrolling content , up to now , We didn't do anything about scrolling Events .
Next , Let's continue to add something to the container element
<div
className="virtualList"
style={{
height: `${height}px`,
}}
onScroll={onListScroll}
ref={scrollRef as LegacyRef<HTMLDivElement>}
>
<div className="listContent" ref={listContentRef as LegacyRef<HTMLDivElement>} > {sourceData.map((item, index) => { return ( <div className="virtualListRow" style={{ height: `${itemHeight}px`, }} key={item.key} > {renderItem(item, index)} </div> ); })} </div> </div>
Copy code
We render this part of the code , Very routine operation , Render all the data ,itemHeight
From the outside , Indicates the height of each line ,renderItem
Also from the outside , What the user wants to render each line , from renderItem decision , Here we are , We seem to have done something , It's like I didn't do anything ( Because the most important part is handed over to the user ).
Virtual list
1. Spread the rolling height
We know that virtual lists render only part of the data , So how to open the scroll bar , Make it look as high as when rendering all the data ? At this time, we need an element to support the height
<div
className="virtualList"
style={{
height: `${height}px`,
}}
onScroll={onListScroll}
ref={scrollRef as LegacyRef<HTMLDivElement>}
>
<div className="listContent" ref={listContentRef as LegacyRef<HTMLDivElement>} > {sourceData.map((item, index) => { return ( <div className="virtualListRow" style={{ height: `${itemHeight}px`, }} key={item.key} > {renderItem(item, index)} </div> ); })} </div> {/* Space occupying elements */} <div style={{ height: totalHeight }}/> </div>
Copy code
totalHeight
How to ?sourceData.length * itemHeight
, At this time listContent
Set to absolute positioning .
.virtualList{
position: relative;
width: 100%;
overflow: auto;
.listContent{
position: absolute;
width: 100%;
z-index: 9;
}
.virtualListRow{
position: relative;
}
}
Copy code
2. Render some data
Next , We need to calculate what data needs to be rendered on the page , Let's first look at what data needs to be rendered during initialization .
const [position, setPosition] = useState<IPosition>({ start: 0, end: 0 });
/** Get the amount of data that needs to be rendered on the page */
const getRenderCount = useCallback(
(data: any[]) => {
if (height) {
return Math.ceil(height / itemHeight);
}
// Render all
return sourceData.length;
},
[height, itemHeight, sourceData.length],
);
/** Generally speaking, after the page is rendered ,renderCount Is constant , To avoid recalculating , Save this value */
const renderCountRef = useRef<number>(0);
// initialization Calculation position
useEffect(() => {
const renderCount = getRenderCount(sourceDataRef.current);
renderCountRef.current = renderCount;
setPosition({ start: 0, end: renderCount });
}, [getRenderCount]);
Copy code
Use area height height
Divided by the height of each line itemHeight
, Get the amount of data you need to render when you first load , We use position This state is used to record the initial position and end position of rendering .
/** Get the data finally rendered to the page */
const renderData = useMemo(() => {
return sourceData.slice(position.start, position.end);
}, [position, sourceData]);
Copy code
renderData
Is the data we want to render to the page . Then the code becomes like this :
<div
className="virtualList"
style={{
height: `${height}px`,
}}
onScroll={onListScroll}
ref={scrollRef as LegacyRef<HTMLDivElement>}
>
<div className="listContent" ref={listContentRef as LegacyRef<HTMLDivElement>} > {renderData.map((item, index) => { return ( <div className="virtualListRow" style={{ height: `${itemHeight}px`, }} key={item.key} > {renderItem(item, position.start + index)} </div> ); })} </div> {/* Space occupying elements */} <div style={{ height: totalHeight }}/> </div>
Copy code
Be careful :renderItem(item, index)
Turned into ,renderItem(item, position.start + index)
, Because we're going to pass it on to renderItem It is always this row of data in sourceData Index in , Not in renderData Middle index .
3. Handle scrolling logic
The above code realizes the rendering of some data , But after the scroll bar scrolls , When the data rolls out of the visual area, it's gone , therefore , Next we need to deal with the logic of scrolling
const onListScroll=(e: UIEvent<HTMLDivElement>) => {
const target = e.target as any;
const { scrollTop } = target;
/** Number of list items obscured */
const overflowNum = computedOverflowCount(sourceData, scrollTop, itemHeight);
/** When scrolling to the next list item , Reset the start of the rendering 、 End list item location */
if (overflowNum !== position.start && listContentRef.current) {
const diff = overflowNum;
// Within the offset quantity, there is no need to set transform, In addition to the offset quantity, you need to set
if (diff > 0) {
setPosition({
start: diff,
end: diff + renderCountRef.current,
});
// adopt transform attribute , Reset the position to the original position , Create the illusion of rolling up all the time
listContentRef.current.style.transform = `translate3d(0,${computedTranslate( sourceData, diff, itemHeight, )}px,0)`;
} else {
setPosition({
start: 0,
end: overflowNum + renderCountRef.current,
});
listContentRef.current.style.transform = `translate3d(0,0,0)`;
}
}
}
Copy code
First, we need to calculate the rolling height scrollTop
when , How many lines are covered .
/** * Calculate the number of rows that are obscured ( Dissatisfied with one , Do not count in ) * @param overflowHeight The height of the covered area * @param itemHeight */
const computedOverflowCount = ( overflowHeight: number, itemHeight: IItemHeight, ) => {
return Math.floor(overflowHeight / itemHeight);
};
Copy code
When you roll to the next line , Reset the start of the rendering 、 End line position , Covered by a few lines , The starting position is a few , End position is Number of lines covered + The number of lines that the visual area can render
.
setPosition({
start: diff,
end: diff + renderCountRef.current,
});
// adopt transform attribute , Reset the position to the original position , Create the illusion of rolling up all the time
listContentRef.current.style.transform = `translate3d(0,${computedTranslate( sourceData, diff, itemHeight, )}px,0)`;
Copy code
In order to keep the data we render in the visual area all the time , We need to pass transform
Attribute to put listContent Move the element down , Number of lines covered * Row height
.
/** * Calculation Translate Distance traveled * @param count How many * @param itemHeight Row height * @returns */
const computedTranslate = ( count: number, itemHeight: IItemHeight, ) => {
return count * itemHeight;
};
Copy code
such , We have completed a basic virtual list .
Dynamic row height
The height of the upper row itemHeight
It's a fixed value , But in many cases, the height of each row may not be fixed , At this time, we allow itemHeight
It's a function , To dynamically calculate the row height
itemHeight:number | ((rowData: any) => number);
Copy code
All areas involving row height need to be judged as number Type or function, Such as :
/** * Calculate the row height of each row * @param rowData Row data * @param itemHeight Row height * @returns */
const computedRowHeight = (rowData: any, itemHeight: IItemHeight) => {
if (typeof itemHeight === 'function') {
return itemHeight(rowData);
}
return itemHeight;
};
/** * Calculate the number of rows that are obscured ( Dissatisfied with one , Do not count in ) * @param sourceData Source data * @param overflowHeight The height of the covered area * @param itemHeight Row height */
const computedOverflowCount = ( sourceData: any[], overflowHeight: number, itemHeight: IItemHeight, ) => {
let count = 0;
let height = 0;
if (typeof itemHeight === 'function') {
sourceData.every((item) => {
if (height <= overflowHeight) {
height += itemHeight(item);
count += 1;
return true;
}
return false;
});
return count;
}
return Math.floor(overflowHeight / itemHeight);
};
Copy code
pre-render
When scrolling is fast , There will be a brief blank , To optimize this problem , We can pre render some data outside the visual area ,.
interface IVirtualList {
... Omit ...
/** Quantity offset */
offsetCount?: {
before: number;
after: number;
};
}
... Omit ...
// initialization Calculation position, Need to render part of the data , namely offsetCount.after Set the number of data rows
useEffect(() => {
const renderCount = getRenderCount(sourceDataRef.current);
renderCountRef.current = renderCount;
setPosition({ start: 0, end: renderCount + offsetCount.after });
}, [getRenderCount, offsetCount.after]);
const onListScroll=(e: UIEvent<HTMLDivElement>) => {
... Omit ...
if (overflowNum !== position.start && listContentRef.current) {
// You need to subtract the pre rendered part of the head , Because pre rendered lines don't have to be destroyed , So set position Count the number of pre renderings when
const diff = overflowNum - offsetCount.before;
if (diff > 0) {
setPosition({
start: diff,
end: overflowNum + renderCountRef.current + offsetCount.after,
});
// After the pre rendered ones are rolled up ,listContent Don't move your position
listContentRef.current.style.transform = `translate3d(0,${computedTranslate( sourceData, offsetCount.before, diff, itemHeight, )}px,0)`;
} else {
... Omit ...
}
}
}
Copy code
External scroll
At present, the virtual list components on the market are components Inside
With scroll bar , The virtual list is realized by monitoring the changes of the internal scroll bar .
Now there is a requirement for components external
An element has a scroll bar , When scrolling the outer scroll bar , Lists can present virtual effects .
Realization principle
: Whether it's scrolling inside the component or scrolling outside the component , The implementation principle is the same , It's just that when the component scrolls outside , The user needs to transfer the external scrolling elements to the inside of the component , So that the component can listen to the rolling event of the element .
// stay interface Join in scrollTarget Field
interface IVirtualList {
... Omit ...
/** Height In case of component internal scrolling, this value is required */
height?: number;
/** External scroll bar object id perhaps ref object ( Only implement first id This situation ) */
scrollTarget?: string;
... Omit ...
}
/** stay getRenderCount Some processing needs to be done in the function , Get the number of data that can be rendered in the scrolling visual area */
const getRenderCount = useCallback(
(data: any[]) => {
// Scroll outside the component
if (scrollTarget) {
const scrollDom = document.getElementById(scrollTarget);
const scrollDomHeight = scrollDom?.offsetHeight!;
// If itemHeight by function, Need to dynamically calculate the renderable height
if (typeof itemHeight === 'function') {
return computedRenderCount(data, scrollDomHeight, itemHeight);
}
return Math.ceil(scrollDomHeight / itemHeight);
}
// Component internal scrolling
if (typeof height === 'number') {
... Omit ...
}
// Render all
return sourceData.length;
},
[height, itemHeight, sourceData.length, scrollTarget],
);
Copy code
Add listening events for external scrolling
/** Bind listening event */
useEffect(() => {
let scrollDom: HTMLElement | null = null;
const listenerHandler = () => onOuterScroll(scrollDom);
if (scrollTarget) {
/** Get the rolling element object */
scrollDom = document.getElementById(scrollTarget);
scrollDom?.addEventListener('scroll', listenerHandler);
}
return () => {
if (scrollDom) {
scrollDom.removeEventListener('scroll', listenerHandler);
}
};
}, [onOuterScroll, scrollTarget]);
Copy code
onOuterScroll
The code of the function is as follows :
const onOuterScroll=(scrollDom: HTMLElement | null)=>{
if (scrollDom) {
// Gets the distance between the inner container element and the target scroll element
const distance = getDistance(scrollRef.current!, scrollDom);
// initial position
const initPosition = { start: 0, end: renderCountRef.current + offsetCount.after };
if (distance < 0) {
const absDistance = Math.abs(distance);
const overflowNum = computedOverflowCount(sourceData, absDistance, itemHeight);
// When scrolling to the next list item ,
if (overflowNum !== position.start && listContentRef.current) {
const diff = overflowNum - offsetCount.before;
// Within the offset quantity, there is no need to set transform, There is no need to reset the start of rendering 、 End list item location
if (diff > 0) {
setPosition({
start: diff,
end: overflowNum + initPosition.end,
});
// adopt transform attribute , Reset the position to the original position , Create the illusion of rolling up all the time
listContentRef.current.style.transform = `translate3d(0,${computedTranslate( sourceData, offsetCount.before, diff, itemHeight, )}px,0)`;
} else {
... Omit Code is the same as internal scrolling ...
}
}
}
// If there is no return to the initial value , It needs to be set to the initial value
else if (position.start !== initPosition.start || position.end !== initPosition.end) {
setPosition({ ...initPosition });
if (listContentRef.current) {
listContentRef.current.style.transform = `translate3d(0,0,0)`;
}
}
}
}
Copy code
Be careful :
The difference between external scrolling and internal scrolling is , Inside, we scroll through scrollTop Directly calculate the hidden row tree overflowNum, But when the outside scrolls , We need to get The distance between the inner container element and the target scroll element
To calculate overflowNum,distance It could be a positive number , It could also be negative .
Pictured :
- Internal scrolling
- External scroll ,distance In the case of positive numbers
- External scroll ,distance negative
such , We implemented a monitor for external scrolling , Realize the function of virtual list .
The effect is as follows :
We can see on the right , When scrolling, some elements do not change . New elements change , The rest of the elements remain the same , Avoid unnecessary rendering .
copyright notice
author[Diao Min hero],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2022/04/202204291628133292.html
The sidebar is recommended
- HTTP becomes HTTPS, self-made certificate
- Web front-end operation - tourism enterprise marketing publicity responsive website template (HTML + CSS + JavaScript)
- Self inspection list of a [qualified] front-end Engineer
- This principle in JavaScript and six common usage scenarios
- JavaScript this priority
- Analyzing the principle of HTTPS encryption
- Difference and principle between websocket and http
- Use of elementui scroll bar component El scrollbar
- Nginx security optimization
- GAC group has become the first pilot enterprise of "yueyouhang". Blessed are the car owners in Guangdong!
guess what you like
Loki HTTP API usage
JavaScript - prototype, prototype chain
Front end experience
JavaScript -- Inheritance
HTTP cache
Filters usage of table in elementui
A JavaScript pit encountered by a non front-end siege lion
Grain College - image error when writing Vue with vscode
Utility gadget - get the IP address in the HTTP request
Could not fetch URL https://pypi.org/simple/pytest-html/: There was a problem confirming the ssl cer
Random recommended
- Function of host parameter in http
- Use nginx proxy node red under centos7 and realize password access
- Centos7 nginx reverse proxy TCP port
- In eclipse, an error is reported when referencing layuijs and CSS
- Front end online teacher Pink
- Learn to use PHP to insert elements at the specified position and key of the array
- Learn how to use HTML and CSS styles to overlay two pictures on another picture to achieve the effect of scanning QR code by wechat
- Learn how to use CSS to vertically center the content in Div
- Learn how to use CSS to circle numbers
- Learn to open and display PDF files in HTML web pages
- The PHP array random sorting function shuffle() randomly scrambles the order of array elements
- JQuery implements the keyboard enter search function
- 16 ArcGIS API for JavaScript 4.18 a new development method based on ES modules @ ArcGIS / core
- 17 ArcGIS API for JavaScript 4.18 draw points, lines and faces with the mouse
- 18 ArcGIS API for JavaScript 4.18 obtain the length and area after drawing line segments and surface features
- Vue environment construction -- scaffold
- Build a demo with Vue scaffold
- Using vuex in Vue projects
- Use Vue router in Vue project
- 26 [react basics-5] react hook
- 07 Chrome browser captures hover element style
- WebGIS development training (ArcGIS API for JavaScript)
- Solution to the blank page of the project created by create react app after packaging
- 19. Html2canvas implements ArcGIS API for JavaScript 4 X screenshot function
- Introduction to JavaScript API for ArcGIS 13
- Development of ArcGIS API for JavaScript under mainstream front-end framework
- Nginx learning notes
- Less learning notes tutorial
- Vue2 cannot get the value in props in the data of the child component, or it is always the default value (the value of the parent component comes from asynchrony)
- LeetCode 217. Determine whether there are duplicate elements in the array
- I'll write a website collection station myself. Do you think it's ok? [HTML + CSS + JS] Tan Zi
- Front end browser debugging tips
- Application of anti chattering and throttling in JavaScript
- How to create JavaScript custom events
- Several ways of hiding elements in CSS
- node. Js-3 step out the use of a server and express package
- CSS matrix function
- Fastapi series - synchronous and asynchronous mutual conversion processing practice
- How to extend the functionality of Axios without interceptors
- Read pseudo classes and pseudo elements