一緒に創造し、成長するために一緒に働きましょう!「ナゲッツデイリー新プラン・8月アップデートチャレンジ」参加2日目、イベント詳細はこちら
この記事は ahoos のソースコードを簡単な言葉で説明する連載の第 18 回で、ドキュメントアドレスにまとめられています。悪くないと思います。星を付けてサポートしてください。ありがとうございます。
序章
仮想化されたリスト機能を提供するフックを使用して、大量のデータをレンダリングする際に最初の画面のレンダリングが遅くなり、スクロールがフリーズするという問題を解決します。
実施原則
その実装原理は、外側のコンテナーのスクロール イベントを監視し、そのサイズが変更されると、計算ロジックをトリガーして、内側のコンテナーの高さと marginTop の値を計算します。
実装
その監視スクロール ロジックは次のとおりです。
// 当外部容器的 size 发生变化的时候,触发计算逻辑
useEffect(() => {
if (!size?.width || !size?.height) {
return;
}
// 重新计算逻辑
calculateRange();
}, [size?.width, size?.height, list]);
// 监听外部容器的 scroll 事件
useEventListener(
'scroll',
e => {
// 如果是直接跳转,则不需要重新计算
if (scrollTriggerByScrollToFunc.current) {
scrollTriggerByScrollToFunc.current = false;
return;
}
e.preventDefault();
// 计算
calculateRange();
},
{
// 外部容器
target: containerTarget,
},
);
复制代码
その中で、calculateRange は非常に重要で、基本的には仮想スクロールのメイン プロセス ロジックを実装し、主に次のことを行います。
- 内部コンテナー全体の totalHeight を取得します。
- 外側のコンテナーの scrollTop に従って「スクロール」されたアイテムの数を計算します。値はオフセットされます。
- 外側のコンテナーの高さと現在の開始インデックスに従って、外側のコンテナーが運ぶことができる visibleCount を取得します。
- そして、オーバースキャン (ビューポートの上下に表示される DOM ノードの数) に従って、開始インデックス (start) と (end) を計算します。
- 開始インデックスに従って、その距離 (offsetTop) の開始までの距離を取得します。
- 最後に、offsetTop と totalHeight に従って、内部コンテナーの高さと marginTop を設定します。
多くの変数があり、次の図を組み合わせることで明確に理解できます。
コードは以下のように表示されます:
// 计算范围,由哪个开始,哪个结束
const calculateRange = () => {
// 获取外部和内部容器
// 外部容器
const container = getTargetElement(containerTarget);
// 内部容器
const wrapper = getTargetElement(wrapperTarget);
if (container && wrapper) {
const {
// 滚动距离顶部的距离。设置或获取位于对象最顶端和窗口中可见内容的最顶端之间的距离
scrollTop,
// 内容可视区域的高度
clientHeight,
} = container;
// 根据外部容器的 scrollTop 算出已经“滚过”多少项
const offset = getOffset(scrollTop);
// 可视区域的 DOM 个数
const visibleCount = getVisibleCount(clientHeight, offset);
// 开始的下标
const start = Math.max(0, offset - overscan);
// 结束的下标
const end = Math.min(list.length, offset + visibleCount + overscan);
// 获取上方高度
const offsetTop = getDistanceTop(start);
// 设置内部容器的高度,总的高度 - 上方高度
// @ts-ignore
wrapper.style.height = totalHeight - offsetTop + 'px';
// margin top 为上方高度
// @ts-ignore
wrapper.style.marginTop = offsetTop + 'px';
// 设置最后显示的 List
setTargetList(
list.slice(start, end).map((ele, index) => ({
data: ele,
index: index + start,
})),
);
}
};
复制代码
その他は、次のようなこの関数のヘルパー関数です。
- 外側の容器と内側の各アイテムの高さに基づいて、可視領域の量を計算します。
// 根据外部容器以及内部每一项的高度,计算出可视区域内的数量
const getVisibleCount = (containerHeight: number, fromIndex: number) => {
// 知道每一行的高度 - number 类型,则根据容器计算
if (isNumber(itemHeightRef.current)) {
return Math.ceil(containerHeight / itemHeightRef.current);
}
// 动态指定每个元素的高度情况
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;
}
}
// 最后一个的下标减去开始一个的下标
return endIndex - fromIndex;
};
复制代码
- scrollTop に基づいて DOM ノードの数を計算します。
// 根据 scrollTop 计算上面有多少个 DOM 节点
const getOffset = (scrollTop: number) => {
// 每一项固定高度
if (isNumber(itemHeightRef.current)) {
return Math.floor(scrollTop / itemHeightRef.current) + 1;
}
// 动态指定每个元素的高度情况
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;
}
// 动态指定每个元素的高度情况,则 itemHeightRef.current 为函数
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;
}
// 动态指定每个元素的高度情况
// @ts-ignore
return list.reduce(
(sum, _, index) => sum + itemHeightRef.current(index, list[index]),
0,
);
}, [list]);
复制代码
最後に、指定されたインデックスまでスクロールする関数が公開されます。これは主に、インデックスの上部から高さ scrollTop を計算し、それを外側のコンテナーに設定します。そして、calculateRange 関数をトリガーします。
// 滚动到指定的 index
const scrollTo = (index: number) => {
const container = getTargetElement(containerTarget);
if (container) {
scrollTriggerByScrollToFunc.current = true;
// 滚动
container.scrollTop = getDistanceTop(index);
calculateRange();
}
};
复制代码
考えてまとめる
高さが比較的確実な場合、仮想スクロールを行うのは比較的簡単ですが、高さが不確かな場合はどうなるでしょうか?
または別の角度で、スクロールが垂直ではなく水平の場合、どのように対処すればよいでしょうか?
この記事は個人のブログに含まれています。注目してください〜