面试题答案
一键面试设计思路
- 计算可见区域:通过监听滚动事件,结合容器高度和列表项高度,计算出当前可见区域的起始和结束索引。
- 渲染可见项:只渲染可见区域内的列表项,减少 DOM 数量,提升性能。
- 列表项复用:利用 React 的 key 机制和状态管理,实现列表项的复用,避免重复渲染相同数据。
- 动态更新:使用 React 的状态更新机制,确保数据变化时,能正确更新可见区域内的列表项。
关键实现步骤
- 创建容器和样式:
在 CSS 中设置容器的高度和 overflow 属性,使其可滚动:import React, { useState, useRef, useEffect } from'react'; import './styles.css'; const VirtualList = () => { const containerRef = useRef(null); return ( <div className="virtual - list - container" ref={containerRef}> {/* 列表项将在这里渲染 */} </div> ); }; export default VirtualList;
.virtual - list - container { height: 400px; overflow - y: scroll; }
2. **计算可见区域**:
```jsx
const VirtualList = () => {
const containerRef = useRef(null);
const [startIndex, setStartIndex] = useState(0);
const [endIndex, setEndIndex] = useState(10);
const itemHeight = 50;
useEffect(() => {
const handleScroll = () => {
const scrollTop = containerRef.current.scrollTop;
const newStartIndex = Math.floor(scrollTop / itemHeight);
const newEndIndex = Math.min(newStartIndex + Math.floor(containerRef.current.clientHeight / itemHeight), data.length);
setStartIndex(newStartIndex);
setEndIndex(newEndIndex);
};
containerRef.current.addEventListener('scroll', handleScroll);
return () => {
containerRef.current.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<div className="virtual - list - container" ref={containerRef}>
{/* 列表项将在这里渲染 */}
</div>
);
};
- 渲染可见项:
在 CSS 中设置列表项样式:const data = Array.from({ length: 10000 }, (_, i) => `Item ${i}`); const VirtualList = () => { const containerRef = useRef(null); const [startIndex, setStartIndex] = useState(0); const [endIndex, setEndIndex] = useState(10); const itemHeight = 50; useEffect(() => { const handleScroll = () => { const scrollTop = containerRef.current.scrollTop; const newStartIndex = Math.floor(scrollTop / itemHeight); const newEndIndex = Math.min(newStartIndex + Math.floor(containerRef.current.clientHeight / itemHeight), data.length); setStartIndex(newStartIndex); setEndIndex(newEndIndex); }; containerRef.current.addEventListener('scroll', handleScroll); return () => { containerRef.current.removeEventListener('scroll', handleScroll); }; }, []); const visibleItems = data.slice(startIndex, endIndex); return ( <div className="virtual - list - container" ref={containerRef}> {visibleItems.map((item, index) => ( <div key={startIndex + index} className="list - item">{item}</div> ))} </div> ); };
.list - item { height: 50px; border - bottom: 1px solid #ccc; display: flex; align - items: center; padding: 0 10px; }
4. **列表项复用**:
通过设置合适的 key,React 可以复用列表项。这里使用 `startIndex + index` 作为 key,确保每个可见项有唯一标识。如果列表项内有更复杂的状态管理,可以使用 `useState` 或 `useReducer` 来管理。
5. **动态更新**:
如果数据发生变化,例如新数据添加到 `data` 数组中,可以通过更新 `data` 状态触发重新渲染。由于 `startIndex` 和 `endIndex` 依赖于滚动位置,可见区域会正确更新:
```jsx
const [data, setData] = useState(Array.from({ length: 10000 }, (_, i) => `Item ${i}`));
const addNewItem = () => {
setData([...data, `New Item ${data.length}`]);
};
然后在组件中添加一个按钮来触发 addNewItem
函数:
return (
<div>
<button onClick={addNewItem}>Add New Item</button>
<div className="virtual - list - container" ref={containerRef}>
{visibleItems.map((item, index) => (
<div key={startIndex + index} className="list - item">{item}</div>
))}
</div>
</div>
);
不同场景下性能优化
-
列表项高度不一致:
- 方案一:记录每个项的高度:
创建一个数组记录每个列表项的高度,例如
const itemHeights = Array.from({ length: data.length }, () => Math.floor(Math.random() * 50) + 30);
。在计算startIndex
和endIndex
时,根据累计高度来计算:useEffect(() => { const handleScroll = () => { let scrollTop = containerRef.current.scrollTop; let newStartIndex = 0; let totalHeight = 0; for (let i = 0; i < itemHeights.length; i++) { totalHeight += itemHeights[i]; if (totalHeight > scrollTop) { newStartIndex = i; break; } } let newEndIndex = newStartIndex; totalHeight = 0; while (totalHeight < containerRef.current.clientHeight && newEndIndex < itemHeights.length) { totalHeight += itemHeights[newEndIndex]; newEndIndex++; } setStartIndex(newStartIndex); setEndIndex(newEndIndex); }; containerRef.current.addEventListener('scroll', handleScroll); return () => { containerRef.current.removeEventListener('scroll', handleScroll); }; }, []);
- 方案二:使用近似高度:
计算平均高度,在计算可见区域时使用平均高度进行近似计算。然后在渲染时,根据实际高度调整布局。例如:
渲染时,使用绝对定位和const averageHeight = itemHeights.reduce((acc, height) => acc + height, 0) / itemHeights.length; useEffect(() => { const handleScroll = () => { const scrollTop = containerRef.current.scrollTop; const newStartIndex = Math.floor(scrollTop / averageHeight); const newEndIndex = Math.min(newStartIndex + Math.floor(containerRef.current.clientHeight / averageHeight), data.length); setStartIndex(newStartIndex); setEndIndex(newEndIndex); }; containerRef.current.addEventListener('scroll', handleScroll); return () => { containerRef.current.removeEventListener('scroll', handleScroll); }; }, []);
transform
来调整列表项位置,以适应实际高度。
- 方案一:记录每个项的高度:
创建一个数组记录每个列表项的高度,例如
-
大数据量下的性能优化:
- 减少重渲染:
使用
React.memo
包裹列表项组件,防止不必要的重渲染。例如:const ListItem = React.memo(({ item }) => { return <div className="list - item">{item}</div>; });
- 虚拟化库:
可以使用成熟的虚拟化库,如
react - virtualized
或react - window
,它们已经对虚拟列表进行了高度优化,支持多种场景,包括列表项高度不一致、固定头部和脚部等。例如使用react - window
:import { FixedSizeList } from'react - window'; const renderRow = ({ index, key, style }) => { return ( <div key={key} style={style}> {data[index]} </div> ); }; const VirtualList = () => { return ( <FixedSizeList height={400} itemCount={data.length} itemSize={50} width={300} > {renderRow} </FixedSizeList> ); };
- 减少重渲染:
使用