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>
相关推荐
qq_456001651 分钟前
30、Vuex 为啥可以进行缓存处理
前端
浪裡遊22 分钟前
Nginx快速上手
运维·前端·后端·nginx
天生我材必有用_吴用32 分钟前
Vue3后台管理项目封装一个功能完善的图标icon选择器Vue组件动态加载icon文件下的svg图标文件
前端
小p41 分钟前
初探typescript装饰器在一些场景中的应用
前端·typescript·nestjs
晓得迷路了1 小时前
栗子前端技术周刊第 72 期 - 快 10 倍的 TypeScript、React Router 7.3、Astro 5.5...
前端·javascript·typescript
xiaoyan20151 小时前
vue3仿Deepseek/ChatGPT流式聊天AI界面,对接deepseek/OpenAI API
前端·vue.js·deepseek
加个鸡腿儿1 小时前
D老师:如何正确控制图片尺寸?父容器设置为何失效?
前端·css
渔樵江渚上1 小时前
深入理解 Web Worker
前端·javascript·面试
JustHappy1 小时前
「工具链🛠️」package-lock.json? yarn.lock? pnpm-lock.yaml?这些文件存在的意义是什么?
前端·javascript·代码规范
KARL1 小时前
最小闭环manus,langchainjs+mcp-client+mcp-server
前端·人工智能