網(wǎng)站首頁 編程語言 正文
簡介
提供虛擬化列表能力的 Hook,用于解決展示海量數(shù)據(jù)渲染時首屏渲染緩慢和滾動卡頓問題。
詳情可見官網(wǎng),文章源代碼可以點(diǎn)擊這里。
實(shí)現(xiàn)原理
其實(shí)現(xiàn)原理監(jiān)聽外部容器的 scroll 事件以及其 size 發(fā)生變化的時候,觸發(fā)計算邏輯算出內(nèi)部容器的高度和 marginTop 值。
具體實(shí)現(xiàn)
其監(jiān)聽滾動邏輯如下:
// 當(dāng)外部容器的 size 發(fā)生變化的時候,觸發(fā)計算邏輯
useEffect(() => {
if (!size?.width || !size?.height) {
return;
}
// 重新計算邏輯
calculateRange();
}, [size?.width, size?.height, list]);
// 監(jiān)聽外部容器的 scroll 事件
useEventListener(
'scroll',
e => {
// 如果是直接跳轉(zhuǎn),則不需要重新計算
if (scrollTriggerByScrollToFunc.current) {
scrollTriggerByScrollToFunc.current = false;
return;
}
e.preventDefault();
// 計算
calculateRange();
},
{
// 外部容器
target: containerTarget,
},
);
其中 calculateRange 非常重要,它基本實(shí)現(xiàn)了虛擬滾動的主流程邏輯,其主要做了以下的事情:
- 獲取到整個內(nèi)部容器的高度 totalHeight。
- 根據(jù)外部容器的 scrollTop 算出已經(jīng)“滾過”多少項,值為 offset。
- 根據(jù)外部容器高度以及當(dāng)前的開始索引,獲取到外部容器能承載的個數(shù) visibleCount。
- 并根據(jù) overscan(視區(qū)上、下額外展示的 DOM 節(jié)點(diǎn)數(shù)量)計算出開始索引(start)和(end)。
- 根據(jù)開始索引獲取到其距離最開始的距離(offsetTop)。
- 最后根據(jù) offsetTop 和 totalHeight 設(shè)置內(nèi)部容器的高度和 marginTop 值。
變量很多,可以結(jié)合下圖,會比較清晰理解:
代碼如下:
// 計算范圍,由哪個開始,哪個結(jié)束
const calculateRange = () => {
// 獲取外部和內(nèi)部容器
// 外部容器
const container = getTargetElement(containerTarget);
// 內(nèi)部容器
const wrapper = getTargetElement(wrapperTarget);
if (container && wrapper) {
const {
// 滾動距離頂部的距離。設(shè)置或獲取位于對象最頂端和窗口中可見內(nèi)容的最頂端之間的距離
scrollTop,
// 內(nèi)容可視區(qū)域的高度
clientHeight,
} = container;
// 根據(jù)外部容器的 scrollTop 算出已經(jīng)“滾過”多少項
const offset = getOffset(scrollTop);
// 可視區(qū)域的 DOM 個數(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,
})),
);
}
};
其它就是這個函數(shù)的輔助函數(shù)了,包括:
- 根據(jù)外部容器以及內(nèi)部每一項的高度,計算出可視區(qū)域內(nèi)的數(shù)量:
// 根據(jù)外部容器以及內(nèi)部每一項的高度,計算出可視區(qū)域內(nèi)的數(shù)量
const getVisibleCount = (containerHeight: number, fromIndex: number) => {
// 知道每一行的高度 - number 類型,則根據(jù)容器計算
if (isNumber(itemHeightRef.current)) {
return Math.ceil(containerHeight / itemHeightRef.current);
}
// 動態(tài)指定每個元素的高度情況
let sum = 0;
let endIndex = 0;
for (let i = fromIndex; i < list.length; i++) {
// 計算每一個 Item 的高度
const height = itemHeightRef.current(i, list[i]);
sum += height;
endIndex = i;
// 大于容器寬度的時候,停止
if (sum >= containerHeight) {
break;
}
}
// 最后一個的下標(biāo)減去開始一個的下標(biāo)
return endIndex - fromIndex;
};
- 根據(jù) scrollTop 計算上面有多少個 DOM 節(jié)點(diǎn):
// 根據(jù) scrollTop 計算上面有多少個 DOM 節(jié)點(diǎn)
const getOffset = (scrollTop: number) => {
// 每一項固定高度
if (isNumber(itemHeightRef.current)) {
return Math.floor(scrollTop / itemHeightRef.current) + 1;
}
// 動態(tài)指定每個元素的高度情況
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;
}
}
// 滿足要求的最后一個 + 1
return offset + 1;
};
- 獲取上部高度:
// 獲取上部高度
const getDistanceTop = (index: number) => {
// 每一項高度相同
if (isNumber(itemHeightRef.current)) {
const height = index * itemHeightRef.current;
return height;
}
// 動態(tài)指定每個元素的高度情況,則 itemHeightRef.current 為函數(shù)
const height = list
.slice(0, index)
// reduce 計算總和
// @ts-ignore
.reduce((sum, _, i) => sum + itemHeightRef.current(i, list[index]), 0);
return height;
};
- 計算總的高度:
// 計算總的高度
const totalHeight = useMemo(() => {
// 每一項高度相同
if (isNumber(itemHeightRef.current)) {
return list.length * itemHeightRef.current;
}
// 動態(tài)指定每個元素的高度情況
// @ts-ignore
return list.reduce(
(sum, _, index) => sum + itemHeightRef.current(index, list[index]),
0,
);
}, [list]);
最后暴露一個滾動到指定的 index 的函數(shù),其主要是計算出該 index 距離頂部的高度 scrollTop,設(shè)置給外部容器。并觸發(fā) calculateRange 函數(shù)。
// 滾動到指定的 index
const scrollTo = (index: number) => {
const container = getTargetElement(containerTarget);
if (container) {
scrollTriggerByScrollToFunc.current = true;
// 滾動
container.scrollTop = getDistanceTop(index);
calculateRange();
}
};
思考總結(jié)
對于高度相對比較確定的情況,我們做虛擬滾動還是相對簡單的,但假如高度不確定呢?
或者換另外一個角度,當(dāng)我們的滾動不是縱向的時候,而是橫向,該如何處理呢?
原文鏈接:https://segmentfault.com/a/1190000042447474
相關(guān)推薦
- 2022-04-05 h5給input元素type=file的對象賦值報錯
- 2023-05-07 GO中什么情況會使用變量逃逸_Golang
- 2023-04-01 PyTorch基礎(chǔ)之torch.nn.CrossEntropyLoss交叉熵?fù)p失_python
- 2022-08-15 Dubbo3基礎(chǔ)配置安裝及整合Springboot
- 2022-09-16 C++中的位運(yùn)算和位圖bitmap解析_C 語言
- 2022-08-25 C/C++內(nèi)存管理基礎(chǔ)與面試_C 語言
- 2022-05-24 Python?6種基本變量操作技巧總結(jié)_python
- 2022-10-26 Python實(shí)戰(zhàn)基礎(chǔ)之Pandas統(tǒng)計某個數(shù)據(jù)列的空值個數(shù)_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支