// 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 }; }