ahooks useVirtualList 封裝虛擬滾動(dòng)列表
簡(jiǎn)介
提供虛擬化列表能力的 Hook,用于解決展示海量數(shù)據(jù)渲染時(shí)首屏渲染緩慢和滾動(dòng)卡頓問題。
詳情可見官網(wǎng),文章源代碼可以點(diǎn)擊這里。
實(shí)現(xiàn)原理
其實(shí)現(xiàn)原理監(jiān)聽外部容器的 scroll 事件以及其 size 發(fā)生變化的時(shí)候,觸發(fā)計(jì)算邏輯算出內(nèi)部容器的高度和 marginTop 值。
具體實(shí)現(xiàn)
其監(jiān)聽滾動(dòng)邏輯如下:
// 當(dāng)外部容器的 size 發(fā)生變化的時(shí)候,觸發(fā)計(jì)算邏輯 useEffect(() => { if (!size?.width || !size?.height) { return; } // 重新計(jì)算邏輯 calculateRange(); }, [size?.width, size?.height, list]); // 監(jiān)聽外部容器的 scroll 事件 useEventListener( 'scroll', e => { // 如果是直接跳轉(zhuǎn),則不需要重新計(jì)算 if (scrollTriggerByScrollToFunc.current) { scrollTriggerByScrollToFunc.current = false; return; } e.preventDefault(); // 計(jì)算 calculateRange(); }, { // 外部容器 target: containerTarget, }, );
其中 calculateRange 非常重要,它基本實(shí)現(xiàn)了虛擬滾動(dòng)的主流程邏輯,其主要做了以下的事情:
- 獲取到整個(gè)內(nèi)部容器的高度 totalHeight。
- 根據(jù)外部容器的 scrollTop 算出已經(jīng)“滾過”多少項(xiàng),值為 offset。
- 根據(jù)外部容器高度以及當(dāng)前的開始索引,獲取到外部容器能承載的個(gè)數(shù) visibleCount。
- 并根據(jù) overscan(視區(qū)上、下額外展示的 DOM 節(jié)點(diǎn)數(shù)量)計(jì)算出開始索引(start)和(end)。
- 根據(jù)開始索引獲取到其距離最開始的距離(offsetTop)。
- 最后根據(jù) offsetTop 和 totalHeight 設(shè)置內(nèi)部容器的高度和 marginTop 值。
變量很多,可以結(jié)合下圖,會(huì)比較清晰理解:
代碼如下:
// 計(jì)算范圍,由哪個(gè)開始,哪個(gè)結(jié)束 const calculateRange = () => { // 獲取外部和內(nèi)部容器 // 外部容器 const container = getTargetElement(containerTarget); // 內(nèi)部容器 const wrapper = getTargetElement(wrapperTarget); if (container && wrapper) { const { // 滾動(dòng)距離頂部的距離。設(shè)置或獲取位于對(duì)象最頂端和窗口中可見內(nèi)容的最頂端之間的距離 scrollTop, // 內(nèi)容可視區(qū)域的高度 clientHeight, } = container; // 根據(jù)外部容器的 scrollTop 算出已經(jīng)“滾過”多少項(xiàng) const offset = getOffset(scrollTop); // 可視區(qū)域的 DOM 個(gè)數(shù) const visibleCount = getVisibleCount(clientHeight, offset); // 開始的下標(biāo) const start = Math.max(0, offset - overscan); // 結(jié)束的下標(biāo) const end = Math.min(list.length, offset + visibleCount + overscan); // 獲取上方高度 const offsetTop = getDistanceTop(start); // 設(shè)置內(nèi)部容器的高度,總的高度 - 上方高度 // @ts-ignore wrapper.style.height = totalHeight - offsetTop + 'px'; // margin top 為上方高度 // @ts-ignore wrapper.style.marginTop = offsetTop + 'px'; // 設(shè)置最后顯示的 List setTargetList( list.slice(start, end).map((ele, index) => ({ data: ele, index: index + start, })), ); } };
其它就是這個(gè)函數(shù)的輔助函數(shù)了,包括:
- 根據(jù)外部容器以及內(nèi)部每一項(xiàng)的高度,計(jì)算出可視區(qū)域內(nèi)的數(shù)量:
// 根據(jù)外部容器以及內(nèi)部每一項(xiàng)的高度,計(jì)算出可視區(qū)域內(nèi)的數(shù)量 const getVisibleCount = (containerHeight: number, fromIndex: number) => { // 知道每一行的高度 - number 類型,則根據(jù)容器計(jì)算 if (isNumber(itemHeightRef.current)) { return Math.ceil(containerHeight / itemHeightRef.current); } // 動(dòng)態(tài)指定每個(gè)元素的高度情況 let sum = 0; let endIndex = 0; for (let i = fromIndex; i < list.length; i++) { // 計(jì)算每一個(gè) Item 的高度 const height = itemHeightRef.current(i, list[i]); sum += height; endIndex = i; // 大于容器寬度的時(shí)候,停止 if (sum >= containerHeight) { break; } } // 最后一個(gè)的下標(biāo)減去開始一個(gè)的下標(biāo) return endIndex - fromIndex; };
- 根據(jù) scrollTop 計(jì)算上面有多少個(gè) DOM 節(jié)點(diǎn):
// 根據(jù) scrollTop 計(jì)算上面有多少個(gè) DOM 節(jié)點(diǎn) const getOffset = (scrollTop: number) => { // 每一項(xiàng)固定高度 if (isNumber(itemHeightRef.current)) { return Math.floor(scrollTop / itemHeightRef.current) + 1; } // 動(dòng)態(tài)指定每個(gè)元素的高度情況 let sum = 0; let offset = 0; // 從 0 開始 for (let i = 0; i < list.length; i++) { const height = itemHeightRef.current(i, list[i]); sum += height; if (sum >= scrollTop) { offset = i; break; } } // 滿足要求的最后一個(gè) + 1 return offset + 1; };
- 獲取上部高度:
// 獲取上部高度 const getDistanceTop = (index: number) => { // 每一項(xiàng)高度相同 if (isNumber(itemHeightRef.current)) { const height = index * itemHeightRef.current; return height; } // 動(dòng)態(tài)指定每個(gè)元素的高度情況,則 itemHeightRef.current 為函數(shù) const height = list .slice(0, index) // reduce 計(jì)算總和 // @ts-ignore .reduce((sum, _, i) => sum + itemHeightRef.current(i, list[index]), 0); return height; };
- 計(jì)算總的高度:
// 計(jì)算總的高度 const totalHeight = useMemo(() => { // 每一項(xiàng)高度相同 if (isNumber(itemHeightRef.current)) { return list.length * itemHeightRef.current; } // 動(dòng)態(tài)指定每個(gè)元素的高度情況 // @ts-ignore return list.reduce( (sum, _, index) => sum + itemHeightRef.current(index, list[index]), 0, ); }, [list]);
最后暴露一個(gè)滾動(dòng)到指定的 index 的函數(shù),其主要是計(jì)算出該 index 距離頂部的高度 scrollTop,設(shè)置給外部容器。并觸發(fā) calculateRange 函數(shù)。
// 滾動(dòng)到指定的 index const scrollTo = (index: number) => { const container = getTargetElement(containerTarget); if (container) { scrollTriggerByScrollToFunc.current = true; // 滾動(dòng) container.scrollTop = getDistanceTop(index); calculateRange(); } };
思考總結(jié)
對(duì)于高度相對(duì)比較確定的情況,我們做虛擬滾動(dòng)還是相對(duì)簡(jiǎn)單的,但假如高度不確定呢?
或者換另外一個(gè)角度,當(dāng)我們的滾動(dòng)不是縱向的時(shí)候,而是橫向,該如何處理呢?
以上就是ahooks useVirtualList 封裝虛擬滾動(dòng)列表的詳細(xì)內(nèi)容,更多關(guān)于ahooks useVirtualList封裝的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解React中傳入組件的props改變時(shí)更新組件的幾種實(shí)現(xiàn)方法
這篇文章主要介紹了詳解React中傳入組件的props改變時(shí)更新組件的幾種實(shí)現(xiàn)方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09React組件內(nèi)事件傳參實(shí)現(xiàn)tab切換的示例代碼
本篇文章主要介紹了React組件內(nèi)事件傳參實(shí)現(xiàn)tab切換的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07react?hooks?UI與業(yè)務(wù)邏輯分離必要性技術(shù)方案
這篇文章主要為大家介紹了react?hooks?UI與業(yè)務(wù)邏輯分離必要性技術(shù)方案詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11React?Server?Component混合式渲染問題詳解
React?官方對(duì)?Server?Comopnent?是這樣介紹的:?zero-bundle-size?React?Server?Components,這篇文章主要介紹了React?Server?Component:?混合式渲染,需要的朋友可以參考下2022-12-12react實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽方式
這篇文章主要介紹了react實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08react項(xiàng)目引入antd框架方式以及遇到的一些坑
這篇文章主要介紹了react項(xiàng)目引入antd框架方式以及遇到的一些坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03