Vue3 封裝不定高虛擬列表 hooks

誌翔發表於2024-10-10
// useVirtualList.ts

import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
import type { Ref } from "vue";

interface Config {
  data: Ref<any[]>; // 資料來源
  scrollContainer: string; // 滾動容器的元素選擇器
  actualHeightContainer: string; // 用於撐開高度的元素選擇器
  translateContainer: string; // 用於偏移的元素選擇器
  itmeContainer: string;// 列表項選擇器
  itemHeight: number; // 列表項高度
  size: number; // 每次渲染資料量
}

type HtmlElType = HTMLElement | null;

export default function useVirtualList(config: Config) {
  // 獲取元素
  let actualHeightContainerEl: HtmlElType = null,
    translateContainerEl: HtmlElType = null,
    scrollContainerEl: HtmlElType = null;

  onMounted(() => {
    actualHeightContainerEl = document.querySelector(
      config.actualHeightContainer
    );
    scrollContainerEl = document.querySelector(config.scrollContainer);
    translateContainerEl = document.querySelector(config.translateContainer);
  });

  // 資料來源,便於後續直接訪問
  let dataSource: any[] = [];

  // 資料來源發生變動
  watch(
    () => config.data.value,
    (newVla) => {
      // 更新資料來源
      dataSource = newVla;

      // 計算需要渲染的資料
      updateRenderData(0);
    }
  );

  // 更新實際高度
  const updateActualHeight = () => {
    let actualHeight = 0;
    dataSource.forEach((_, i) => {
      actualHeight += getItemHeightFromCache(i);
    });

    actualHeightContainerEl!.style.height = actualHeight + "px";
  };

  // 快取已渲染元素的高度
  const RenderedItemsCache: any = {};

  // 更新已渲染列表項的快取高度
  const updateRenderedItemCache = (index: number) => {
    // 當所有元素的實際高度更新完畢,就不需要重新計算高度
    const shouldUpdate =
      Object.keys(RenderedItemsCache).length < dataSource.length;
    if (!shouldUpdate) return;

    nextTick(() => {
      // 獲取所有列表項元素
      const Items: HTMLElement[] = Array.from(
        document.querySelectorAll(config.itmeContainer)
      );

      // 進行快取
      Items.forEach((el) => {
        if (!RenderedItemsCache[index]) {
          RenderedItemsCache[index] = el.offsetHeight;
        }
        index++;
      });

      // 更新實際高度
      updateActualHeight();
    });
  };

  // 獲取快取高度,無快取,取配置項的 itemHeight
  const getItemHeightFromCache = (index: number | string) => {
    const val = RenderedItemsCache[index];
    return val === void 0 ? config.itemHeight : val;
  };

  // 實際渲染的資料
  const actualRenderData: Ref<any[]> = ref([]);

  // 更新實際渲染資料
  const updateRenderData = (scrollTop: number) => {
    let startIndex = 0;
    let offsetHeight = 0;

    for (let i = 0; i < dataSource.length; i++) {
      offsetHeight += getItemHeightFromCache(i);

      if (offsetHeight >= scrollTop) {
        startIndex = i;
        break;
      }
    }

    // 計算得出的渲染資料
    actualRenderData.value = dataSource.slice(
      startIndex,
      startIndex + config.size
    );

    // 快取最新的列表項高度
    updateRenderedItemCache(startIndex);

    // 更新偏移值
    updateOffset(offsetHeight - getItemHeightFromCache(startIndex));
  };

  // 更新偏移值
  const updateOffset = (offset: number) => {
    translateContainerEl!.style.transform = `translateY(${offset}px)`;
  };

  // 滾動事件
  const handleScroll = (e: any) => {
    // 渲染正確的資料
    updateRenderData(e.target.scrollTop);
  };

  // 註冊滾動事件
  onMounted(() => {
    scrollContainerEl?.addEventListener("scroll", handleScroll);
  });

  // 移除滾動事件
  onBeforeUnmount(() => {
    scrollContainerEl?.removeEventListener("scroll", handleScroll);
  });

  return { actualRenderData };
}

相關文章