虚拟列表使用


Vue 虚拟列表使用

简介

  • 所有的前端应用中最常见的性能问题就是渲染大型列表。无论一个框架性能有多好,渲染成千上万个列表项都会变得很慢,因为浏览器需要处理大量的 DOM 节点。

  • 但是,我们并不需要立刻渲染出全部的列表。在大多数场景中,用户的屏幕尺寸只会展示这个巨大列表中的一小部分。我们可以通过列表虚拟化来提升性能,这项技术使我们只需要渲染用户视口中能看到的部分。

文件目录

|-- ./src
|   |-- ./src/App.vue
|   |-- ./src/components
|   |   |-- ./src/components/MyTest.vue
|   |   |-- ./src/components/Person.vue
|   |-- ./src/main.js
|   `-- ./src/randomData.js
npm i vue-virtual-scroller

vue-virtual-scroller API:https://github.com/Akryum/vue-virtual-scroller/blob/master/packages/vue-virtual-scroller/README.md

// ./src/main.js
import { createApp } from "vue";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import VueVirtualScroller from "vue-virtual-scroller";
import App from "./App.vue";
const app = createApp(App);
app.use(VueVirtualScroller);
app.mount("#app");
<!-- ./src/App.vue -->
<script setup>
  import MyTest from "./components/MyTest.vue";
</script>

<template>
  <MyTest />
</template>

<style scoped>
  html,
  body,
  #app {
    box-sizing: border-box;
    height: 100%;
  }

  body {
    font-size: 16px;
    font-family: "Avenir", Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    color: #2c3e50;
    margin: 0;
  }

  #app,
  .page {
    display: flex;
    flex-direction: column;
    align-items: stretch;
  }

  .menu,
  .page {
    padding: 12px;
    box-sizing: border-box;
  }

  .package {
    margin-right: 12px;
  }

  .package-name {
    font-family: monospace;
    color: #2c3e50;
    background: #eee;
    padding: 5px 12px 3px;
  }
  .vue-recycle-scroller {
    -webkit-overflow-scrolling: touch;
  }

  .vue-recycle-scroller__item-container,
  .vue-recycle-scroller__item-wrapper {
    box-sizing: border-box;
  }

  .vue-recycle-scroller__item-view {
    cursor: pointer;
    user-select: none;
    -moz-user-select: none;
    -webkit-user-select: none;
  }

  .tr,
  .td {
    box-sizing: border-box;
  }

  .vue-recycle-scroller__item-view .tr {
    display: flex;
    align-items: center;
  }

  .vue-recycle-scroller__item-view .td {
    display: block;
  }

  .toolbar {
    flex: auto 0 0;
    text-align: center;
    margin-bottom: 12px;
    line-height: 32px;
    position: sticky;
    top: 0;
    z-index: 9999;
    background: white;
  }

  .recycle-scroller-demo.page-mode .toolbar {
    border-bottom: solid 1px #e0edfa;
  }

  .toolbar > *:not(:last-child) {
    margin-right: 24px;
  }

  .avatar {
    background: grey;
  }
</style>
// ./src/randomData.js
import { faker } from "@faker-js/faker/locale/zh_CN";

// 处理时间
function handleDate(date) {
  if (!date.getFullYear && !date.getMonth && !date.getDate) return;
  function checkTime(i) {
    if (i < 10) {
      i = "0" + i;
    }
    return i;
  }
  return (
    checkTime(date.getFullYear()) +
    "-" +
    checkTime(date.getMonth() + 1) +
    "-" +
    checkTime(date.getDate())
  );
}

// 生成数据
function generateData(count) {
  if (!count || Number(count) === 0) {
    count = 10;
  }

  const namesArr = [];
  for (let i = 0; i < count; i++) {
    const birthday = faker.date.birthdate({ min: 18, max: 65, mode: "age" });
    namesArr.push({
      id: faker.datatype.uuid(),
      index: i + 1,
      height: 20,
      value: { name: faker.name.fullName(), birthday: handleDate(birthday) },
    });
  }
  return namesArr;
}

export { generateData };
<script setup>
  // ./src/components/MyTest.vue
  import { watch, computed, onMounted, nextTick, ref } from "vue";
  import Person from "./Person.vue";
  import { generateData } from "../randomData";

  const items = ref([]);
  const count = ref(10000);
  const renderScroller = ref(true);
  const showScroller = ref(true);
  const buffer = ref(200);
  const pageMode = ref(true);
  const pageModeFullPage = ref(true);
  const scrollTo = ref(200);
  const scroller = ref(null);

  const list = computed(() => {
    return items.value.map((item) => {
      return Object.assign({}, item);
    });
  });
  const generateItems = () => {
    console.log("Generating " + count.value + " items...");
    const time = Date.now();
    const _items = generateData(count.value);
    console.log(
      "Generated " + _items.length + " in " + (Date.now() - time) + "ms"
    );
    items.value = _items;
  };
  watch(count, () => generateItems());

  onMounted(() => {
    nextTick(() => generateItems());
    window.scroller = scroller.value;
  });
</script>

<template>
  <div
    class="recycle-scroller-demo"
    :class="{
      'page-mode': pageMode,
      'full-page': pageModeFullPage,
    }"
  >
    <div class="toolbar">
      <span>
        <button @mousedown="scroller.scrollToItem(scrollTo)">Scroll To:</button>
        <input
          v-model.number="scrollTo"
          type="number"
          min="0"
          :max="list.length - 1"
        />
      </span>
      <span>
        <button @mousedown="renderScroller = !renderScroller">
          Toggle render
        </button>
        <button @mousedown="showScroller = !showScroller">
          Toggle visibility
        </button>
      </span>
    </div>

    <div
      v-if="renderScroller"
      v-show="showScroller"
      class="content"
    >
      <div class="wrapper">
        <RecycleScroller
          :key="pageModeFullPage"
          ref="scroller"
          class="scroller"
          :items="list"
          :buffer="buffer"
          :page-mode="pageMode"
          key-field="id"
          size-field="height"
        >
          <template #before>
            <h3>表头占位</h3>
          </template>
          <template #default="props">
            <Person
              :item="props.item"
              :index="props.index"
            />
          </template>
        </RecycleScroller>
      </div>
    </div>
    <div v-else>
      <h2>My Components</h2>
    </div>
  </div>
</template>

<style scoped>
  .recycle-scroller-demo:not(.page-mode) {
    height: 100%;
    display: flex;
    flex-direction: column;
  }

  .recycle-scroller-demo.page-mode:not(.full-page) {
    height: 100%;
  }

  .recycle-scroller-demo.page-mode {
    flex: auto 0 0;
  }

  .recycle-scroller-demo.page-mode .toolbar {
    border-bottom: solid 1px #e0edfa;
  }

  .content {
    flex: 100% 1 1;
    border: solid 1px #42b983;
    position: relative;
  }

  .recycle-scroller-demo.page-mode:not(.full-page) .content {
    overflow: auto;
  }

  .recycle-scroller-demo:not(.page-mode) .wrapper {
    overflow: hidden;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
  }

  .scroller {
    width: 100%;
    height: 100%;
  }
</style>
<!-- ./src/components/Person.vue -->
<template>
  <div
    class="tr person"
    @click="edit"
  >
    <div class="td index">{{ index }}</div>
    <div class="td">
      <div class="info">
        <span class="name">{{ item.value.name }}</span>
        <span class="birthday">{{ item.value.birthday }}</span>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    props: ["item", "index"],

    methods: {
      edit() {
        this.item.value.birthday += "#";
      },
    },
  };
</script>

<style scoped>
  .hover > .tr.person {
    background-color: lightblue;
  }
  .tr.person {
    display: flex;
  }
  .index {
    color: lightcoral;
    width: 55px;
    text-align: right;
    flex: auto 0 0;
    margin-right: 20px;
  }

  .person .info {
    display: flex;
    align-items: center;
  }
  .name {
    margin-right: 20px;
  }
</style>

文章作者: PaoMo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 PaoMo !
  目录