useVirtualizedList
虚拟列表 Hook
#代码演示
#相同尺寸列表项
itemSize
表示列表项的尺寸,当滚动方向为 vertical
时,表示高度;当滚动方向为 horizontal
时,表示宽度
horizontal
设置为true
则变为水平布局模式,默认为false
pinOffset
表示方向变化后是否保持offset
scrollToOffset()
滚动到指定偏移方法scrollToIndex()
滚动到指定项方法
保持 offset:
start
Live Editor
Copy
function Example() {const containerRef = useRef(null)const [data] = useState(Array.from({ length: 1000000 }, (_, i) => i))const [options, setOptions] = useState({horizontal: false,pinOffset: false})const refs = useRef({scrollToIndex: 0,scrollToOffset: 0,scrollToAlign: 'start'})const {list,wrapperStyle,scrollToIndex,scrollToOffset} = useVirtualizedList(data, {itemSize: 30,containerRef,horizontal: options.horizontal,pinOffset: options.pinOffset})return (<div><divref={containerRef}style={{width: !options.horizontal ? 150 : 250,height: !options.horizontal ? 250 : 150,overflow: 'auto',border: '1px solid #dadde1'}}><ulstyle={{...wrapperStyle,margin: 0}}>{list.map(({ data, style, index }) => {return (<likey={index}style={{...style,background: index % 2 ? '#f1f1f1' : '#fff',display: 'flex',justifyContent: 'center',alignItems: 'center',color: '#1c1e21',margin: 0}}>{data}</li>)})}</ul></div><div style={{ marginTop: 20 }}><div>保持 offset:<Radio.GrouponChange={e =>setOptions({...options,pinOffset: e.target.value})}value={options.pinOffset}><Radio value={true}>是</Radio><Radio value={false}>否</Radio></Radio.Group><Buttonsize="small"onClick={() =>setOptions({...options,horizontal: !options.horizontal})}>切换方向</Button></div><div style={{ marginTop: 10, display: 'flex', alignItems: 'center' }}><InputNumbersize="small"min={0}defaultValue={refs.current.scrollToIndex}onChange={index => (refs.current.scrollToIndex = index)}/><Selectsize="small"value={refs.current.scrollToAlign}onChange={align => (refs.current.scrollToAlign = align)}style={{ marginLeft: 10 }}><Select.Option value="start">start</Select.Option><Select.Option value="center">center</Select.Option><Select.Option value="end">end</Select.Option><Select.Option value="auto">auto</Select.Option></Select><Buttonsize="small"onClick={() =>scrollToIndex(refs.current.scrollToIndex, {align: refs.current.scrollToAlign})}style={{ marginLeft: 10 }}>scrollToIndex</Button></div><div style={{ marginTop: 10, display: 'flex', alignItems: 'center' }}><InputNumbersize="small"min={0}defaultValue={refs.current.scrollToOffset}onChange={offset => (refs.current.scrollToOffset = offset)}/><Buttonsize="small"onClick={() => scrollToOffset(refs.current.scrollToOffset)}style={{ marginLeft: 10 }}>scrollToOffset</Button></div></div></div>)}
#非相同尺寸列表项
itemSize
可以设置为一个返回尺寸的纯函数
保持 offset:
start
Live Editor
Copy
function Example() {const containerRef = useRef(null)const [data] = useState(Array.from({ length: 1000000 }, (_, i) => i))const [options, setOptions] = useState({horizontal: false,pinOffset: false})const refs = useRef({scrollToIndex: 0,scrollToOffset: 0,scrollToAlign: 'start'})const {list,wrapperStyle,scrollToIndex,scrollToOffset} = useVirtualizedList(data, {itemSize: index => (index % 2 ? 30 : 50),containerRef,horizontal: options.horizontal,pinOffset: options.pinOffset})return (<div><divref={containerRef}style={{width: !options.horizontal ? 150 : 250,height: !options.horizontal ? 250 : 150,overflow: 'auto',border: '1px solid #dadde1'}}><ulstyle={{...wrapperStyle,margin: 0}}>{list.map(({ data, style, index }) => {return (<likey={index}style={{...style,background: index % 2 ? '#f1f1f1' : '#fff',display: 'flex',justifyContent: 'center',alignItems: 'center',color: '#1c1e21',margin: 0}}>{data}</li>)})}</ul></div><div style={{ marginTop: 20 }}><div>保持 offset:<Radio.GrouponChange={e =>setOptions({...options,pinOffset: e.target.value})}value={options.pinOffset}><Radio value={true}>是</Radio><Radio value={false}>否</Radio></Radio.Group><Buttonsize="small"onClick={() =>setOptions({...options,horizontal: !options.horizontal})}>切换方向</Button></div><div style={{ marginTop: 10, display: 'flex', alignItems: 'center' }}><InputNumbersize="small"min={0}defaultValue={refs.current.scrollToIndex}onChange={index => (refs.current.scrollToIndex = index)}/><Selectsize="small"value={refs.current.scrollToAlign}onChange={align => (refs.current.scrollToAlign = align)}style={{ marginLeft: 10 }}><Select.Option value="start">start</Select.Option><Select.Option value="center">center</Select.Option><Select.Option value="end">end</Select.Option><Select.Option value="auto">auto</Select.Option></Select><Buttonsize="small"onClick={() =>scrollToIndex(refs.current.scrollToIndex, {align: refs.current.scrollToAlign})}style={{ marginLeft: 10 }}>scrollToIndex</Button></div><div style={{ marginTop: 10, display: 'flex', alignItems: 'center' }}><InputNumbersize="small"min={0}defaultValue={refs.current.scrollToOffset}onChange={offset => (refs.current.scrollToOffset = offset)}/><Buttonsize="small"onClick={() => scrollToOffset(refs.current.scrollToOffset)}style={{ marginLeft: 10 }}>scrollToOffset</Button></div></div></div>)}
#监听触顶 / 触底事件
设置 onTopReached
监听触顶,onTopReachedThreshold
设置触顶偏移;
设置 onEndReached
监听触底,onEndReachedThreshold
设置触底偏移;
Live Editor
Copy
function Example() {const containerRef = useRef(null)const [loading, setLoading] = useState(false)const [moreLoading, setMoreLoading] = useState(false)const [data, setData] = useState(Array.from({ length: 20 }, (_, i) => {return i}))const { list, wrapperStyle } = useVirtualizedList(data, {itemSize: 30,containerRef,onTopReachedThreshold: 30,onTopReached() {setLoading(true)return new Promise(resolve => {const addNum = Math.pow(10, Math.ceil(Math.random() * 5))setTimeout(resolve,500,Array.from({ length: 20 }, (_, i) => {return i + addNum}))}).then(newData => {setData(newData)}).finally(() => {setLoading(false)})},onEndReachedThreshold: 30,onEndReached() {if (moreLoading) returnsetMoreLoading(true)return new Promise(resolve => {const listSlice = list.map(item => item.data)setTimeout(resolve,500,listSlice.concat([listSlice.slice(-1)[0] + 1,listSlice.slice(-1)[0] + 2,listSlice.slice(-1)[0] + 3]))}).then(newData => {setData(newData)}).finally(() => {setMoreLoading(false)})}})return (<div><Spin spinning={loading}><divref={containerRef}style={{width: 150,height: 250,overflow: 'auto',border: '1px solid #dadde1'}}><ulstyle={{...wrapperStyle,margin: 0}}>{list.map(({ data, style, index }) => {return (<likey={index}style={{...style,background: index % 2 ? '#f1f1f1' : '#fff',display: 'flex',justifyContent: 'center',alignItems: 'center',color: '#1c1e21',margin: 0}}>{data}</li>)})}</ul><divstyle={{display: 'flex',justifyContent: 'center',alignItems: 'center',color: '#1c1e21',margin: 0}}>{moreLoading && 'loading'}</div></div></Spin></div>)}
#API
type UseVirtualList = <T = any>(list: T[], options: Options): Result<T>
#Options
参数 | 说明 | 必选 | 类型 | 默认值 |
---|---|---|---|---|
containerRef | 滚动容器的 ref | 是 | React.RefObject<HTMLElement> | - |
horizontal | 设置为 true 则变为水平布局模式 | - | false | boolean |
startOffset | 初始状态列表顶部距离容器顶部的距离,默认为 0,当列表元素上面存在其他元素时需要设置该参数 | - | number | 0 |
pinOffset | 是否在切换方向后保持偏移量 | - | boolean | false |
itemSize | 列表项尺寸:竖向滚动时,设置为高度;横向滚动时,设置为宽度 | 是 | number | ((index: number) => number) | - |
overscanCount | 视区上、下额外渲染的 dom 节点数量 | - | number | 10 |
scrollRenderAheadCount | 视区上、下额外渲染的个数少于该参数设定的值时,重新计算整个渲染列表 | - | number | 5 |
onTopReached | 监听触顶 | - | () => Promise<any> | - |
onTopReachedThreshold | 调用 onTopReached 之前的临界值 | - | number | 0 |
onEndReached | 监听触底 | - | () => Promise<any> | - |
onEndReachedThreshold | 触底偏移量 | - | number | 0 |
onScroll | 监听滚动 | - | (event: Event, scrollOffset: number) => void | - |
#Result
参数 | 说明 | 类型 |
---|---|---|
list | 要渲染的列表片段 | {data: T; index: number; style: React.CSSProperties}[] |
isScrolling | 是否正在滚动 | boolean |
wrapperStyle | 容器的样式 | React.CSSProperties |
refreshing | 表示触顶方法执行中 | boolean |
reachEndPending | 表示触底方法执行中 | boolean |
scrollToOffset | 滚动到指定偏移方法 | (offset: number) => void |
scrollToIndex | 滚动到指定项方法 | (index: number, options?: { align?: 'start' | 'center' | 'end' | 'auto' }) => void |
getVisibleSlice | 获取当前可视区域渲染的元素切片 | () => number[] |
updateSize | 更新元素尺寸,用于动态计算元素场景 | (index: number, size: number) => void |