誰もが理解できるソース コードは、仮想スクロール リストをカプセル化した使用 VirtualList です

一緒に創造し、成長するために一緒に働きましょう!「ナゲッツデイリー新プラン・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();
  }
};
复制代码

考えてまとめる

高さが比較的確実な場合、仮想スクロールを行うのは比較的簡単ですが、高さが不確かな場合はどうなるでしょうか?

または別の角度で、スクロールが垂直ではなく水平の場合、どのように対処すればよいでしょうか?

この記事は個人のブログに含まれています。注目してください〜

おすすめ

転載: juejin.im/post/7132842601786376200