Skip to content

Commit

Permalink
feat: V2, 性能提升
Browse files Browse the repository at this point in the history
  • Loading branch information
jamie6689 committed Jun 12, 2024
1 parent 95a98cc commit e656643
Show file tree
Hide file tree
Showing 15 changed files with 277 additions and 33 deletions.
6 changes: 3 additions & 3 deletions build/asset-manifest.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"files": {
"main.css": "/react-virtualized-list/build/static/css/main.91612258.css",
"main.js": "/react-virtualized-list/build/static/js/main.a584e1cc.js",
"main.js": "/react-virtualized-list/build/static/js/main.a3122816.js",
"static/js/845.6038aacd.chunk.js": "/react-virtualized-list/build/static/js/845.6038aacd.chunk.js",
"index.html": "/react-virtualized-list/build/index.html",
"main.91612258.css.map": "/react-virtualized-list/build/static/css/main.91612258.css.map",
"main.a584e1cc.js.map": "/react-virtualized-list/build/static/js/main.a584e1cc.js.map",
"main.a3122816.js.map": "/react-virtualized-list/build/static/js/main.a3122816.js.map",
"845.6038aacd.chunk.js.map": "/react-virtualized-list/build/static/js/845.6038aacd.chunk.js.map"
},
"entrypoints": [
"static/css/main.91612258.css",
"static/js/main.a584e1cc.js"
"static/js/main.a3122816.js"
]
}
2 changes: 1 addition & 1 deletion build/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/react-virtualized-list/build/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/react-virtualized-list/build/logo192.png"/><link rel="manifest" href="/react-virtualized-list/build/manifest.json"/><title>React App</title><script defer="defer" src="/react-virtualized-list/build/static/js/main.a584e1cc.js"></script><link href="/react-virtualized-list/build/static/css/main.91612258.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/react-virtualized-list/build/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/react-virtualized-list/build/logo192.png"/><link rel="manifest" href="/react-virtualized-list/build/manifest.json"/><title>React App</title><script defer="defer" src="/react-virtualized-list/build/static/js/main.a3122816.js"></script><link href="/react-virtualized-list/build/static/css/main.91612258.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/bundle.cjs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/bundle.esm.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { terser } from 'rollup-plugin-terser';

// eslint-disable-next-line import/no-anonymous-default-export
export default {
input: 'src/VirtualizedList/index.js',
input: 'src/VirtualizedListV2/index.js',
output: [
{
file: 'lib/bundle.cjs.js',
Expand Down
122 changes: 122 additions & 0 deletions src/VirtualizedListV2/VirtualizedList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import useIntersectionObserver from './useIntersectionObserver';
import VirtualizedListItem from './VirtualizedListItem';

const BUFFER_SIZE = 2;

const VirtualizedList = ({
listData = [],
renderItem = (itemData) => (<>{itemData ? itemData : 'Loading data...'}</>),
refreshOnVisible = false,
fetchItemData = null,
containerHeight = '400px',
itemStyle = {},
listClassName = null,
itemClassName = null,
observerOptions = { root: null, rootMargin: '0px', threshold: 0.1 },
onLoadMore = () => {},
hasMore = false,
loader = '',
endMessage = '',
itemLoader = '',
emptyListMessage = null,
}) => {
const [visibleItems, setVisibleItems] = useState(new Set());
const [loading, setLoading] = useState(false);
const containerRef = useRef([]);
const sentinelRef = useRef(null);

const handleVisibilityChange = useCallback((isVisible, entry) => {
const index = parseInt(entry.target.getAttribute('data-index'), 10);
if (isVisible) {
setVisibleItems(prev => new Set(prev).add(index));
} else {
setVisibleItems(prev => {
const newVisibleItems = new Set(prev);
newVisibleItems.delete(index);
return newVisibleItems;
});
}
}, []);

const { observe, unobserve } = useIntersectionObserver(containerRef.current, handleVisibilityChange, null, observerOptions);

const handleSentinelIntersection = useCallback((isVisible) => {
if (isVisible && hasMore && onLoadMore && !loading) {
setLoading(true);
onLoadMore().finally(() => {
setLoading(false);
});
}
}, [hasMore, onLoadMore, loading]);

useIntersectionObserver([sentinelRef.current], handleSentinelIntersection, null, { root: null, rootMargin: '0px', threshold: 1.0 });

const visibleRange = useMemo(() => {
const sortedVisibleItems = [...visibleItems].sort((a, b) => a - b);
if (sortedVisibleItems.length === 0) return [0, BUFFER_SIZE];
const firstVisible = sortedVisibleItems[0];
const lastVisible = sortedVisibleItems[sortedVisibleItems.length - 1];
return [Math.max(0, firstVisible - BUFFER_SIZE), Math.min(listData.length - 1, lastVisible + BUFFER_SIZE)];
}, [visibleItems, listData.length]);

useEffect(() => {
containerRef.current.forEach((node, index) => {
if (node && (index < visibleRange[0] || index > visibleRange[1])) {
unobserve(node);
containerRef.current[index] = null;
}
});
}, [visibleRange, unobserve]);

const handleRef = useCallback((node, index) => {
if (node) {
containerRef.current[index] = node;
observe(node);
} else {
if (containerRef.current[index]) {
unobserve(containerRef.current[index]);
containerRef.current[index] = null;
}
}
}, [observe, unobserve]);

const itemContainerStyle = useMemo(() => ({
...itemStyle,
}), [itemStyle]);

return (
<div className={listClassName} style={{ height: containerHeight, overflowY: 'auto' }}>
{listData.length ? listData.map((item, index) => (
(index >= visibleRange[0] && index <= visibleRange[1]) ? (
<div
className={itemClassName}
style={itemContainerStyle}
ref={node => handleRef(node, index)}
key={index}
data-index={index}
>
<VirtualizedListItem
item={listData[index]}
isVisible={visibleItems.has(index)}
refreshOnVisible={refreshOnVisible}
fetchItemData={fetchItemData}
itemLoader={itemLoader}
>
{renderItem}
</VirtualizedListItem>
</div>
) : null
)) : (
emptyListMessage ? emptyListMessage : null
)}
{ listData.length ? hasMore ? (
<div ref={sentinelRef} style={{ height: '1px' }}>{loader}</div>
) : (
<div>{endMessage}</div>
) : null }
</div>
);
};

export default VirtualizedList;
23 changes: 23 additions & 0 deletions src/VirtualizedListV2/VirtualizedListItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { useState, useEffect, useRef } from 'react';

const VirtualizedListItem = ({ item, isVisible, refreshOnVisible, fetchItemData, children, itemLoader }) => {
const [data, setData] = useState(null);
const hasRequested = useRef(false);

useEffect(() => {
if (fetchItemData && isVisible && (refreshOnVisible || !hasRequested.current)) {
fetchItemData(item).then(data => {
setData(data);
hasRequested.current = true;
});
}
}, [isVisible, refreshOnVisible, item]);

return (
<>
{isVisible ? children(item, data) : itemLoader}
</>
);
};

export default VirtualizedListItem;
6 changes: 6 additions & 0 deletions src/VirtualizedListV2/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import VirtualizedList from './VirtualizedList';
import useIntersectionObserver from './useIntersectionObserver';

export { useIntersectionObserver };

export default VirtualizedList;
65 changes: 65 additions & 0 deletions src/VirtualizedListV2/useIntersectionObserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';

const useIntersectionObserver = (nodes, onVisibilityChange, onEntryUpdate, options = {}) => {
const defaultOptions = {
root: null,
rootMargin: '0px',
threshold: 0.1,
};

const observerRef = useRef(null);
const intersectingStates = useRef(new Map());

const memoizedOptions = useMemo(() => ({ ...defaultOptions, ...options }), [options]);
const memoizedOnVisibilityChange = useCallback(onVisibilityChange, [onVisibilityChange]);
const memoizedOnEntryUpdate = useCallback(onEntryUpdate, [onEntryUpdate]);

useEffect(() => {
if (observerRef.current) {
observerRef.current.disconnect();
}

observerRef.current = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (memoizedOnEntryUpdate) memoizedOnEntryUpdate(entry);

const prevIntersecting = intersectingStates.current.get(entry.target);
const currIntersecting = entry.isIntersecting;
if (prevIntersecting !== currIntersecting) {
intersectingStates.current.set(entry.target, currIntersecting);
if (memoizedOnVisibilityChange) memoizedOnVisibilityChange(currIntersecting, entry);
}
});
}, memoizedOptions);

if (nodes) {
nodes.forEach(node => {
if (node) {
observerRef.current.observe(node);
}
});
}

return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [nodes, memoizedOnVisibilityChange, memoizedOnEntryUpdate, memoizedOptions]);

const observe = useCallback((node) => {
if (observerRef.current && node) {
observerRef.current.observe(node);
}
}, []);

const unobserve = useCallback((node) => {
if (observerRef.current && node) {
observerRef.current.unobserve(node);
}
}, []);

return { observe, unobserve };
};

export default useIntersectionObserver;
11 changes: 11 additions & 0 deletions src/VirtualizedListV2/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func(...args);
}, delay);
};
};

export { debounce };
Loading

0 comments on commit e656643

Please sign in to comment.