优化滚动列表,使用虚拟滚动

实现虚拟滚动功能可以帮助优化页面性能,尤其是在处理大量数据(如长列表或表格)时。虚拟滚动的核心思想是只渲染当前视口(Viewport)内可见的元素,而不是一次性渲染所有元素。这样可以显著减少DOM操作的开销,提升页面的加载速度和滚动性能。

以下是实现虚拟滚动的详细步骤和代码示例,我们将使用纯JavaScript来实现一个简单的虚拟滚动列表。

1. 计算视口大小和元素高度

视口大小:当前可视区域的高度。 元素高度:每个列表项的高度。

js 复制代码
// 获取元素高度
const boxHeight = getPx(itemHeight.value) + getPx(marginBottom.value);
// 获取视口高度
nextTick(async () => {
    const res = await getRect(".hy-virtual-container");
    viewHeight.value = (res as UniApp.NodeInfo).height ?? 0;
  });

2. 计算当前可视区域内的元素范围

起始索引:当前可视区域的第一个元素的索引。 结束索引:当前可视区域的最后一个元素的索引。

javascript 复制代码
// 起始索引
const start = computed(() => {
  const s = Math.floor(scrollTop.value / boxHeight);
  return Math.max(0, s * line.value);
});
javascript 复制代码
// 结束索引
const over = computed(() => {
  const o = Math.floor(
    (scrollTop.value + viewHeight.value + 1) / boxHeight + 5,
  );
  return Math.min(list.value.length, o * line.value);
});

4. 初始化渲染

截取需要展示的数据列表

javascript 复制代码
const virtualData = computed(() => {
  return list.value.slice(start.value, over.value);
});

5. 监听滚动事件

动态更新:当用户滚动时,根据滚动位置动态更新可视区域内的元素。

js 复制代码
const onScroll = async (e: any) => {
  scrollTop.value = e.detail.scrollTop ?? 0;
};

6.完整代码如下:

html 复制代码
<template>
  <scroll-view
    ref="hyVirtualContainer"
    @scroll="onScroll"
    @scrolltolower="scrollToLower"
    :lower-threshold="showDivider ? 40 : 10"
    :scroll-y="true"
    scroll-with-animation
    class="hy-virtual-container"
  >
    <view class="hy-virtual-container__list">
      <slot
        v-if="slotDefault"
        :record="line === 1 ? virtualData : waterfall"
      ></slot>
      <template v-else>
        <view
          v-if="line === 1"
          class="hy-virtual-container__list--item"
          v-for="(item, i) in virtualData"
          :key="typeof item === 'string' ? i : item[keyField]"
          :style="itemStyle"
          @click="handleClick(item)"
        >
          <slot style="height: 100%" name="content" :record="item"></slot>
        </view>

        <view
          v-if="line === 2"
          class="hy-virtual-container__list--left hy-virtual-container__list--box"
        >
          <view
            v-if="slots.left"
            class="hy-virtual-container__list--box-item"
            v-for="item in waterfall.left"
            :key="item[keyField]"
            :style="itemStyle"
            @click="handleClick(item)"
          >
            <slot name="left" :record="item"></slot>
          </view>
          <slot v-else name="left-list" :record="waterfall.left"> </slot>
        </view>
        <view
          v-if="line === 2"
          class="hy-virtual-container__list--right hy-virtual-container__list--box"
        >
          <view
            v-if="slots.right"
            class="hy-virtual-container__list--box-item"
            v-for="item in waterfall.right"
            :key="item[keyField]"
            :style="itemStyle"
            @click="handleClick(item)"
          >
            <slot name="right" :record="item"></slot>
          </view>
          <slot v-else name="right-list" :record="waterfall.right"> </slot>
        </view>
      </template>
      <!--加载更多样式-->
    </view>
    <!--    <HyDivider :text="load" v-if="showDivider"></HyDivider>-->
  </scroll-view>
</template>

<script lang="ts">
export default {
  options: {
    virtualHost: true,
  },
};
</script>

<script lang="ts" setup>
import {
  computed,
  type CSSProperties,
  nextTick,
  onMounted,
  reactive,
  ref,
  toRefs,
  useSlots,
  watch,
} from "vue";
import { addUnit, getPx, getRect } from "../../utils";
import type IProps from "./typing";
import defaultProps from "./props";

const props = withDefaults(defineProps<IProps>(), defaultProps);
const {
  list,
  line,
  keyField,
  itemHeight,
  containerHeight,
  marginBottom,
  padding,
  borderRadius,
  background,
  border,
} = toRefs(props);
const emit = defineEmits(["scrollButton", "click"]);

const slots = useSlots();
// 滚动条距离顶部距离
const scrollTop = ref(0);
// 可视区域的高度
const viewHeight = ref(0);
const waterfall: { left: AnyObject[]; right: AnyObject[] } = reactive({
  left: [],
  right: [],
});
// 排列方式
const arrange = computed(() => (line.value === 1 ? "column" : "row"));
const boxHeight = getPx(itemHeight.value) + getPx(marginBottom.value);
const listHeight = addUnit(containerHeight.value);

onMounted(() => {
  nextTick(async () => {
    const res = await getRect(".hy-virtual-container");
    viewHeight.value = (res as UniApp.NodeInfo).height ?? 0;
  });
});

const itemStyle = computed((): CSSProperties => {
  return {
    height: addUnit(itemHeight.value),
    padding: addUnit(padding.value),
    marginBottom: addUnit(marginBottom.value),
    borderRadius: addUnit(borderRadius.value),
    background: background.value,
    border: border.value ? "1px solid #dadbde" : "",
  };
});

/**
 * @description 虚拟列表真实展示数据:起始下标
 */
const start = computed(() => {
  const s = Math.floor(scrollTop.value / boxHeight);
  return Math.max(0, s * line.value);
});

/**
 * @description 虚拟列表真实展示数据:结束下标
 */
const over = computed(() => {
  const o = Math.floor(
    (scrollTop.value + viewHeight.value + 1) / boxHeight + 5,
  );
  return Math.min(list.value.length, o * line.value);
});

/**
 * @description 计算虚拟列表的padding(保持列表高度完整且滚动条能正常滚动)
 */
const paddingAttr = computed(() => {
  const paddingTop = start.value * boxHeight;
  const paddingBottom = (list.value.length - over.value) * boxHeight;
  return `${paddingTop / line.value}px 0 ${paddingBottom / line.value}px`;
});

/**
 * @description 虚拟列表真实展示数据
 */
const virtualData = computed(() => {
  return list.value.slice(start.value, over.value);
});

watch(
  () => virtualData.value,
  (newVal, oldValue) => {
    waterfall.left.length = 0;
    waterfall.right.length = 0;
    if (line.value === 2 && newVal.every((item) => typeof item !== "string")) {
      newVal.forEach((item, i) => {
        if (i % 2 === 0) {
          waterfall.left.push(item);
        } else {
          waterfall.right.push(item);
        }
      });
    }
  },
  { immediate: true, deep: true },
);

/**
 * @description 监听滚动条距离顶部距离,实时更新
 */
const onScroll = async (e: any) => {
  scrollTop.value = e.detail.scrollTop ?? 0;
};

/**
 * @description 滚动底部函数
 * */
const scrollToLower = () => {
  emit("scrollButton");
};

/**
 * @description 点击行触发函数
 * */
const handleClick = (temp: string | AnyObject) => {
  emit("click", temp);
};

/**
 * @description 获取默认插槽
 */
const slotDefault = useSlots().default;
</script>

<style lang="scss" scoped>
@use "../../theme.scss" as *;
@use "../../libs/css/mixin.scss" as *;

.hy-virtual-container {
  height: v-bind(listHeight);
  padding: 0 $hy-border-margin-padding-base;
  box-sizing: border-box;

  &__list {
    padding: v-bind(paddingAttr);
    @include flex(v-bind(arrange));
    overflow-anchor: none;
    &--item {
      box-sizing: border-box;
    }

    &--left {
      margin-right: $hy-border-margin-padding-base;
    }
    &--box {
      box-sizing: border-box;
      width: 50%;
      display: flex;
      flex-direction: column;
      &-item {
        box-sizing: border-box;
        position: relative;
        overflow: hidden;
      }
    }
  }
}
</style>

如果你对实现细节仍有疑问,可以直接访问华玥组件库的虚拟列表组件页面,查看其源码以获取更多信息: 华玥组件库 虚拟列表List

相关推荐
袋鱼不重18 分钟前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
Fireworks40 分钟前
深入vue3源码解读 -- 1、响应式的基础概念
前端
程序员黑豆40 分钟前
JDK 下载安装与配置详细教程
java·前端·ai编程
hunterandroid1 小时前
文件存储:内部存储与外部存储
前端
NorBugs1 小时前
飞机大战 Low 版 (Made in AI)
前端
angerdream2 小时前
Android手把手编写儿童手机远程监控App之agentweb如何实现全屏
前端
星栈2 小时前
10 分钟跑起第一个 Dioxus 应用:`dx` CLI、`rsx!` 和热更新好不好用
前端·rust·前端框架
奋斗吧程序媛2 小时前
补充一个小知识点:有关@click.native
前端·vue.js
触底反弹2 小时前
🚀 手把手用 HTML5 Canvas 从零打造飞机大战游戏,代码全开源!
前端·javascript·canvas
DJ斯特拉2 小时前
axios快速使用
开发语言·前端·javascript