vue虚拟列表实现

146 阅读1分钟

定高虚拟列表

// useVirtualList.ts

import { ref, onMounted, onBeforeUnmount, watch, computed} from "vue";

import type { Ref } from "vue";

interface Config {
  data: Ref<any[]>; // 数据
  itemHeight: number;// 列表项高度
  size: number;// 每次渲染数据量
  scrollContainer: string;// 滚动容器的元素选择器
  actualHeightContainer: string;// 用于撑开高度的元素选择器
  tranlateContainer: string;// 用于偏移的元素选择器
}

type HtmlElType = HTMLElement | null;

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

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

  // 通过设置高度,模拟滚动
  watch(() => config.data.value, (newVal) => {
    actualHeightContainerEl!.style.height = newVal.length * config.itemHeight + "px";
  });

  // 实际渲染的数据
  const startIndex = ref(0);
  const endIndex = ref(config.size - 1);
  const actualRenderData = computed(() => {
    return config.data.value.slice(startIndex.value, endIndex.value + 1);
  });

  // 滚动事件
  const handleScroll = (e) => {
    const target = e.target;
    const { scrollTop, clientHeight, scrollHeight } = target;

    // 边界控制:实际触底,且页面正常渲染全部数据时,不再触发后续计算,防止触底抖动
    if (
      scrollHeight <= scrollTop + clientHeight &&
      endIndex.value >= config.data.value.length
    ) {
      return;
    }

    // 保证数据渲染一直在可视区
    tranlateContainerEl.style.transform = `translateY(${scrollTop}px)`;

    // 渲染正确的数据
    startIndex.value = Math.floor(scrollTop / config.itemHeight);
    endIndex.value = startIndex.value + config.size;
  };

  // 注册滚动事件
  onMounted(() => {
    scrollContainerEl?.addEventListener("scroll", handleScroll);
  });

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

  return { actualRenderData };
}

列表结构

<ul class="scroll-container"> // 滚动容器 
<div class="actual-height-container">// 渲染实际高度的容器
<div class="tranlate-container"> // 用于偏移的容器 
<li v-for="(item, i) in actualRenderData"> ... </li> 
</div> 
</div> 
</ul>

针对 el-table 组件 的选择器可用如下的方式:

const { actualRenderData } = useVirtualList({ 
data: tableData, // 列表项数据 
itemHeight: 100, 
size: 10, 
scrollContainer: ".el-scrollbar__wrap", // 滚动容器 
actualHeightContainer: ".el-scrollbar__view", // 渲染实际高度的容器 tranlateContainer: ".el-table__body", // 需要偏移的目标元素 
});

虚拟列表-不定高

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

el-table 组件,如下:

const { actualRenderData } = useVirtualList({
  data: tableData, // 列表项数据
  scrollContainer: ".el-scrollbar__wrap", // 滚动容器
  actualHeightContainer: ".el-scrollbar__view", // 渲染实际高度的容器
  tranlateContainer: ".el-table__body", // 需要偏移的目标元素,
  itmeContainer: '.el-table__row',// 列表项
  itemHeight: 50,// 列表项的大致高度
  size: 10,// 单次渲染数量
});

普通list列表

const { actualRenderData } = useVirtualList({
  data: tableData, // 列表项数据
  scrollContainer: ".scroll-container", // 滚动容器
  actualHeightContainer: ".actual-height-container", // 渲染实际高度的容器
  translateContainer: ".translate-container", // 需要偏移的目标元素,
  itmeContainer: '.item',// 列表项
  itemHeight: 50,// 列表项的大致高度
  size: 10,// 单次渲染数量
});

转载自: 作者:熊的猫
链接:juejin.cn/post/741566…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。