VirtualList虚拟列表

首先感谢 Vue3 封装不定高虚拟列表 hooks,复用性更好!这篇文章提供的一些思路,在此基础作者进一步对相关代码进行了一些性能上的优化(解决了通过鼠标操作滚动条时的卡顿)。因为项目没有用到ts,就先去掉了。

hooks

js 复制代码
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue";

export default function useVirtualList(config) {
  // 获取元素
  let actualHeightContainerEl = null,
    translateContainerEl = null,
    scrollContainerEl = null;
  // 数据源,便于后续直接访问
  let dataSource = [];

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

  // 数据源发生变动
  watch(
    () => config.data.value,
    (newValue) => {
      // 更新数据源
      dataSource = newValue;
      // 计算需要渲染的数据
      updateRenderData();
    }
  );

  /* 
  
  
  
    更新相关逻辑
  
  
  
  */
  // 更新实际高度
  let flag = false;
  const updateActualHeight = (oldValue, value) => {
    let actualHeight = 0;
    if (flag) {
      // 修复偏差
      actualHeight =
        actualHeightContainerEl.offsetHeight -
        (oldValue || config.itemHeight) +
        value;
    } else {
      // 首次渲染
      flag = true;
      for (let i = 0; i < dataSource.length; i++) {
        actualHeight += getItemHeightFromCache(i);
      }
    }
    actualHeightContainerEl.style.height = `${actualHeight}px`;
  };

  // 缓存已渲染元素的高度
  const RenderedItemsCache = {};
  const RenderedItemsCacheProxy = new Proxy(RenderedItemsCache, {
    get(target, key, receiver) {
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      // 更新实际高度
      updateActualHeight(oldValue, value);
      return result;
    },
  });

  // 更新已渲染列表项的缓存高度
  const updateRenderedItemCache = (index) => {
    // 当所有元素的实际高度更新完毕,就不需要重新计算高度
    const shouldUpdate =
      Reflect.ownKeys(RenderedItemsCacheProxy).length < dataSource.length;
    if (!shouldUpdate) return;

    nextTick(() => {
      // 获取所有列表项元素(size条数)
      const Items = Array.from(document.querySelectorAll(config.itemContainer));
      // 进行缓存(通过下标作为key)
      for (let i = 0; i < Items.length; i++) {
        const el = Reflect.get(Items, i);
        const itemIndex = index + i;
        if (!Reflect.get(RenderedItemsCacheProxy, itemIndex)) {
          Reflect.set(RenderedItemsCacheProxy, itemIndex, el.offsetHeight);
        }
      }
    });
  };

  // 获取缓存高度,无缓存,取配置项的 itemHeight
  const getItemHeightFromCache = (index) => {
    const val = Reflect.get(RenderedItemsCacheProxy, index);
    return val === void 0 ? config.itemHeight : val;
  };

  // 实际渲染的数据
  const actualRenderData = ref([]);

  // 更新实际渲染数据
  const updateRenderData = (scrollTop = 0) => {
    let startIndex = 0;
    let offsetHeight = 0;

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

      // 第几个以上进行隐藏
      if (offsetHeight >= scrollTop - (config.offset || 0)) {
        startIndex = i;
        break;
      }
    }
    // 计算得出的渲染数据
    actualRenderData.value = dataSource
      .slice(startIndex, startIndex + config.size)
      .map((data, idx) => {
        return {
          key: startIndex + idx + 1, // 为了在vue的for循环中绑定唯一key值
          data,
        };
      });

    // 缓存最新的列表项高度
    updateRenderedItemCache(startIndex);

    updateOffset(offsetHeight - getItemHeightFromCache(startIndex));
  };

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

  /* 
  
  
  
    注册事件、销毁事件
  
  
  
  */
  // 滚动事件
  const handleScroll = (e) =>
    // 渲染正确的数据
    updateRenderData(e.target.scrollTop);

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

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

  return { actualRenderData };
}

vue

vue 复制代码
<script setup>
import { ref } from "vue";
import useVirtualList from "../utils/useVirtualList.js"; // 上面封装的hooks文件
import list from "../json/index.js"; // 造的数据模拟
const tableData = ref([]);
// 模拟异步请求
setTimeout(() => {
  tableData.value = list;
}, 0);
const { actualRenderData } = useVirtualList({
  data: tableData, // 列表项数据
  scrollContainer: ".scroll-container", // 滚动容器
  actualHeightContainer: ".actual-height-container", // 渲染实际高度的容器
  translateContainer: ".translate-container", // 需要偏移的目标元素,
  itemContainer: ".item", // 列表项
  itemHeight: 400, // 列表项的大致高度
  size: 10, // 单次渲染数量
  offset: 200, // 偏移量
});
</script>

<template>
  <div>
    <h2>virtualList 不固定高度虚拟列表</h2>
    <ul class="scroll-container">
      <div class="actual-height-container">
        <div class="translate-container">
          <li
            v-for="item in actualRenderData"
            :key="item.key"
            class="item"
            :class="[{ 'is-odd': item.key % 2 }]"
          >
            <div class="item-title">第{{ item.key }}条:</div>
            <div>{{ item.data }}</div>
          </li>
        </div>
      </div>
    </ul>
  </div>
</template>

<style scoped>
* {
  list-style: none;
  padding: 0;
  margin: 0;
}
.scroll-container {
  border: 1px solid #000;
  width: 1000px;
  height: 200px;
  overflow: auto;
}
.item {
  border: 1px solid #ccc;
  padding: 20px;
  display: flex;
  flex-wrap: wrap;
  word-break: break-all;
}
.item.is-odd {
  background-color: rgba(0, 0, 0, 0.1);
}
</style>
相关推荐
小小小小宇18 分钟前
前端Loader笔记
前端
烛阴1 小时前
从0到1掌握盒子模型:精准控制网页布局的秘诀
前端·javascript·css
前端工作日常5 小时前
我理解的`npm pack` 和 `npm install <local-path>`
前端
李剑一5 小时前
说个多年老前端都不知道的标签正确玩法——q标签
前端
嘉小华5 小时前
大白话讲解 Android屏幕适配相关概念(dp、px 和 dpi)
前端
姑苏洛言5 小时前
在开发跑腿小程序集成地图时,遇到的坑,MapContext.includePoints(Object object)接口无效在组件中使用无效?
前端
七八书5 小时前
Vue3 组件通信全解析:从基础到进阶的实用指南
vue.js
奇舞精选5 小时前
Prompt 工程实用技巧:掌握高效 AI 交互核心
前端·openai
用户3802258598245 小时前
vue3源码解析:模块总览
vue.js
Danny_FD5 小时前
React中可有可无的优化-对象类型的使用
前端·javascript