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>
相关推荐
计算机毕设-小月哥7 分钟前
【Python Django + Vue】酒店在线预订系统:用技术说话!
开发语言·vue.js·后端·python·django·计算机毕设·计算机毕业设计
云小遥38 分钟前
Cornerstone3D Tools对影像进行交互(中篇)-注释类工具使用
前端·vue.js
前端斌少1 小时前
强大灵活的文件上传库:FilePond 详解
前端·vue·react
OEC小胖胖1 小时前
使用CSS和HTML实现3D图片环绕效果
前端·css·3d·html·web
wy3136228211 小时前
android——Groovy gralde 脚本迁移到DSL
android·前端·javascript
大G哥2 小时前
前端Socket互动小游戏开发体验分享
前端·状态模式
karshey3 小时前
【debug】ElementPlus table组件column传入formatter类型报错
开发语言·前端·javascript
夜空孤狼啸3 小时前
js 通过input,怎么把选择的txt文件转为base64格式
前端·javascript·vue.js
又是重名了3 小时前
ajax地址参数与data参数运用
前端·ajax
执键行天涯3 小时前
【Vue】Vue扫盲(三)计算属性和监听器
前端·javascript·vue.js