Location>code7788 >text

Give me 2 minutes and I promise to teach you to implement a fixed-height virtual list in Vue3

Popularity:435 ℃/2024-12-19 20:47:15

preamble

Virtual list for most of the first-line development students is not at all strange things, some students are directly using third-party components. However, if you write a virtual list on your resume, but give the interviewer that it is realized through a third-party component, the air may be frozen at this time. So this article Ouyang will teach you within 2 minutes to achieve a fixed height of the virtual list, as for the virtual list is not high to write the next article.

Ouyang is also looking for a job and is located in Chengdu!

What is a virtual list

There are special scenarios where we can't paginate and can only render a long list. This long list may have tens of thousands of data, if all rendered to the page on the user's device almost may be directly stuck, then we need virtual list to solve the problem.

A common virtual list is something like the following, shown below:
v1

A solid boxed item indicates that the DOM is actually rendered in the viewport area, and a dashed boxed item indicates that the DOM is not rendered.

In the virtual list of fixed heights, we can define the virtual list according to theHeight of visual arearespond in singingHeight of each itemCalculate how many items can be rendered in the visible area. items that are not in the visible area don't need to be rendered (regardless of whether there are tens of thousands or hundreds of thousands of items), which solves the problem of poor performance of long lists.

Implementing scrollbars

Following the diagram above, it's easy to think that our dom structure should look like the following:

<template>
  <div class="container">
    <div class="list-wrapper">
      <!-- Renders only the visual area list data -->
    </div>
  </div>
</template>

<style scoped>
  .container {
    height: 100%;
    overflow: auto;
    position: relative;
  }
</style>

visual fieldcontainerSet Height100%, which can also be a fixed height value. And set theoverflow: auto;Let the content scroll in the visual area.

At this point we meet the first question, how do the scrollbars come about and what holds the visual area open?

The answer is simple, we know the height of each itemitemSizeand knows how many pieces of data. So.itemSize * Isn't that the true list height. So we can set the height of the list in the visual areacontainerCreate a new file namedplaceholderto an empty div, set his height toitemSize * This way the visual area is propped up and the scrollbars are there. The code is as follows:

<template>
  <div class="container">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <div class="list-wrapper">
      <!-- Renders only the visual area list data -->
    </div>
  </div>
</template>

<script setup>
  import { ref, onMounted, computed } from "vue";
  const { listData, itemSize } = defineProps({
    listData: {
      type: Array,
      default: () => [],
    },
    itemSize: {
      type: Number,
      default: 100,
    },
  });

  const listHeight = computed(() => * itemSize);
</script>

<style scoped>
  .container {
    height: 100%;
    overflow: auto;
    position: relative;
  }
  .placeholder {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: -1;
  }
</style>

placeholderAbsolute positioning is used, and in order not to block the list rendered in the visual area, it is set toz-index: -1

The next step is to calculate how many items are actually rendered inside the container, which is simple.(Height of visible area / height of each item)

Why useWhat about rounding up?

Whenever an item misses a bit out of the visual area, we should render it as well.

At this point we get a couple of variables:

  • start: The value of the index of the first item rendered in the visual area, initialized to 0.

  • renderCount: The number of items rendered in the visual area.

  • end: the index value of the last item rendered in the visual area, which is equal to the value ofstart + renderCount. Note that we are usingstart + renderCountactually renders an extra item, such as thestart = 0cap (a poem)renderCount = 2We set up theend = 2, which actually renders 3 items. the purpose is to pre-render the next one, more on that later.

Listening to scroll events

With the scrollbar in place it's time to start scrolling, and we listen to thecontainerThe container's scroll event.

The content of the visual area should change as the scrollbar scrolls, which means that in the scroll event we need to recalculate thestartThe value of the

function handleScroll(e) {
  const scrollTop = ;
   = (scrollTop / itemSize);
   = scrollTop - (scrollTop % itemSize);
}

If the currentitemSizeThe value of 100.

If the scrolling distance is between 0-100 at this point, like the following:
v2

Above this picture item1 has not been completely rolled out of the visual area, some in the visual area, some in the visual area outside. At this time, the visual area is displayed in theitem1-item7That's why we had to render an extra item when we calculated end earlier, otherwise item7 wouldn't show up here.

The rendered DOM doesn't change when the scroll distance is between 0 and 100, we're completely reusing the browser's scrolling and not processing it in any way.

(coll.) fail (a student)scrollTopThe value of 100 is when item1 has just been rolled outside the visual area. At this point item1 doesn't need to be rendered anymore, because he's no longer visible. So at this point thestartThe value of the0update to1If you are a member of the group, you are not a member of the group, but you are a member of the group.scrollTopThe value of the110The value of start is also the same1. So it follows that = (scrollTop / itemSize);Below:
v3

at this timestartThe rendering starts at item2, but since we reused the browser scrolling earlier, the first DOM that is actually rendered is already outside the visual area. At this point, the first item seen in the visual area is item3, which is obviously incorrect; the first item seen should be item2.

What should we do at this point?

It's easy to usetranslateJust offset the list by one item's height, i.e. 100px, and the list will look like the following after offsetting:
v4

If the currentscrollTophas a value of 200, then the offset is 200px. so we arrive at

 = scrollTop - (scrollTop % itemSize);

Why subtract here?scrollTop % itemSizeAnd?

Because if we scroll within the height of the item, we are multiplexing the browser's scrolling, and there is no need for an offset, so we need to calculate the offset value by subtracting thescrollTop % itemSize

In fact when scrolling from one item to another, such as from theitem0Scroll toitem1. Two things will be done at this point: thestartThe value of the0update to1and on the basis ofscrollTopCalculate to get the offset value of the list100and thus the new start corresponding to theitem1Back in visual range.

This is a running effect image:
demo

Here is the full code:

<template>
  <div ref="container" class="container" @scroll="handleScroll($event)">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <div class="list-wrapper" :style="{ transform: getTransform }">
      <div
        class="card-item"
        v-for="item in renderList"
        :key=""
        :style="{
          height: itemSize + 'px',
          lineHeight: itemSize + 'px',
          backgroundColor: `rgba(0,0,0,${ / 100})`,
        }"
      >
        {{  + 1 }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, computed } from "vue";
const { listData, itemSize } = defineProps({
  listData: {
    type: Array,
    default: () => [],
  },
  itemSize: {
    type: Number,
    default: 100,
  },
});

const container = ref(null);
const containerHeight = ref(0);
const renderCount = computed(() => ( / itemSize));
const start = ref(0);
const offset = ref(0);
const end = computed(() =>  + );
const listHeight = computed(() =>  * itemSize);
const renderList = computed(() => (,  + 1));

const getTransform = computed(() => `translate3d(0,${}px,0)`);

onMounted(() => {
   = ;
});

function handleScroll(e) {
  const scrollTop = ;
   = (scrollTop / itemSize);
   = scrollTop - (scrollTop % itemSize);
}
</script>

<style scoped>
.container {
  height: 100%;
  overflow: auto;
  position: relative;
}

.placeholder {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.card-item {
  padding: 10px;
  color: #777;
  box-sizing: border-box;
  border-bottom: 1px solid #e1e1e1;
}
</style>

This is the code for the parent component:

<template>
  <div style="height: 100vh; width: 100vw">
    <VirtualList :listData="data" :itemSize="100" />
  </div>
</template>

<script setup>
import VirtualList from "./";
import { ref } from "vue";

const data = ref([]);
for (let i = 0; i < 1000; i++) {
  ({ id: i, value: i });
}
</script>

<style>
html {
  height: 100%;
}
body {
  height: 100%;
  margin: 0;
}
#app {
  height: 100%;
}
</style>

summarize

In this article we talked about how to implement a virtual list with a fixed height, first calculate the number of items that can be rendered in the viewport based on the height of the viewing area and the height of the item.renderCount. Then based on the distance rolled to calculate thestartposition, calculate theendThe position of thestart + renderCount Pre-rendering an item, which directly replicates the browser's scrolling when scrolling within the bounds of each item, without having to do any processing. When scrolling from one item to another, two things are done: the start value is updated and the value of thescrollTopCalculates the offset of the list to bring the item corresponding to the new start back into view.

Follow the public number: [Front-end Ouyang], give yourself a chance to advance vue

Also Ouyang wrote an open source ebookvue3 compilation principle revealed, reading this book can give you a qualitative improvement in your knowledge of vue compilation. This book is accessible to beginner and intermediate front-ends and is completely free, just asking for a STAR.