SoFunction
Updated on 2025-04-04

Detailed explanation of the implementation of virtual list components in vue3

1. Introduction

I saw the virtual list in many people’s blogs, but I haven’t studied it carefully. I took advantage of this time to study it carefully. I wonder if anyone will read this article? Just do it hahaha, I hope I can use this article in the future.

This article mainly implements a virtual list with a fixed height, and mainly provides a simple learning of the idea of ​​virtual list.

2. Implementation of Vue3 virtual list component

2.1 Basic ideas

Regarding the implementation of virtual lists, from my perspective, it is divided into three steps:

  • Determine the starting subscript of the intercepted data, and then calculate the end subscript based on the value of the starting subscript.
  • Fill unrendered positions with padding values ​​so that they can slide, while dynamically computing paddingTop and paddingBottom
  • Bind scrolling events, update the intercepted view list and padding values

Here the key point is to calculate the initial subscript, which should be calculated based on the scrollTop attribute (I have written before that the virtual list changes the initial subscript according to the number of scroll changes. As a result, when the scrolling is very fast, it will not correspond at all. Through scrollTop, it is stable, because the value of scrollTop is bound to the position of the scrollbar, and the position of the scrollbar and the number of times the scroll event are triggered are not unique)

2.2 Code implementation

Basic dom structure

<template>
    <div class="scroll-box" ref="scrollBox" @scroll="handleScroll"
    :style="{ height: scrollHeight + 'px' }">
        <div class="virtual-list" :style="{ paddingTop: paddingTop + 'px', paddingBottom: paddingBottom + 'px' }">
            <div v-for="(item, index) in visibleItems" :key="index" :style="{ height: itemHeight + 'px' }">
                <slot name="item" :item="item" :index="index"></slot>
            </div>
        </div>
        <Loading :is-show="isShowLoad" />
    </div>
</template>

In order to ensure that the list item has more freedom, choose to use the slot. When using it, you need to ensure that the height of the list item is consistent with the set height.

Calculate paddingTop, paddingBottom, visibleItems

const visibleCount = ( / ) + 1
const start = ref(0)
const end = computed(() => ( + 2 * visibleCount - 1,))
const paddingTop = computed(() =>  * )
const renderData = ref([...])
const paddingBottom = computed(() => ( - ) * )
const visibleItems = computed(() => (, ))

where scrollHeight refers to the height of the sliding area, itemHeight refers to the height of the list element. Here, in order to avoid white screens, the end value is set to the data of the size of two screens, and the second screen is used as a buffer; define a more renderData variable because there will be more functions to load afterwards, and the value of props is inconvenient to modify (you can use v-model, but it feels not necessary. After all, the list data that the parent element does not need to be updated)

Bind scroll event

let lastIndex = ;
const handleScroll = rafThrottle(() => {
    onScrollToBottom();
    onScrolling();
});
const onScrolling = () => {
    const scrollTop = ;
    let thisStartIndex = (scrollTop / );
    const isSomeStart = thisStartIndex == lastIndex;
    if (isSomeStart) return;
    const isEndIndexOverListLen = thisStartIndex + 2 * visibleCount - 1 >= ;
    if (isEndIndexOverListLen) {
        thisStartIndex =  - (2 * visibleCount - 1);
    }
    lastIndex = thisStartIndex;
     = thisStartIndex;
}
function rafThrottle(fn) {
    let lock = false;
    return function (...args) {
        if (lock) return;
        lock = true;
        (() => {
            (args);
            lock = false;
        });
    };
}

Among them, onScrollToBottom is the bottoming loading more functions. You will know in the code below. Ignore it here. In the scroll event, calculate the starting subscript satrt according to the value of scrollTop, thereby updating the calculation properties paddingTop, paddingBottom, visibleItems, and implementing the virtual list. Here, the request animation frame is also used for throttling optimization.

3. Complete component code

&lt;template&gt;
    &lt;div class="scroll-box" ref="scrollBox" @scroll="handleScroll"
    :style="{ height: scrollHeight + 'px' }"&gt;
        &lt;div class="virtual-list" :style="{ paddingTop: paddingTop + 'px', paddingBottom: paddingBottom + 'px' }"&gt;
            &lt;div v-for="(item, index) in visibleItems" :key="index" :style="{ height: itemHeight + 'px' }"&gt;
                &lt;slot name="item" :item="item" :index="index"&gt;&lt;/slot&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;Loading :is-show="isShowLoad" /&gt;
    &lt;/div&gt;
&lt;/template&gt;
&lt;script setup&gt;
import { ref, computed,onMounted,onUnmounted } from 'vue'
import Loading from './';
import { ElMessage } from 'element-plus'
const props = defineProps({
    listData: { type: Array, default: () =&gt; [] },
    itemHeight: { type: Number, default: 50 },
    scrollHeight: { type: Number, default: 300 },
    loadMore: { type: Function, required: true }
})
const isShowLoad = ref(false);
const visibleCount = ( / ) + 1
const start = ref(0)
const end = computed(() =&gt; ( + 2 * visibleCount - 1,))
const paddingTop = computed(() =&gt;  * )
const renderData = ref([...])
const paddingBottom = computed(() =&gt; ( - ) * )
const visibleItems = computed(() =&gt; (, ))
const scrollBox = ref(null);
let lastIndex = ;
const handleScroll = rafThrottle(() =&gt; {
    onScrollToBottom();
    onScrolling();
});
const onScrolling = () =&gt; {
    const scrollTop = ;
    let thisStartIndex = (scrollTop / );
    const isSomeStart = thisStartIndex == lastIndex;
    if (isSomeStart) return;
    const isEndIndexOverListLen = thisStartIndex + 2 * visibleCount - 1 &gt;= ;
    if (isEndIndexOverListLen) {
        thisStartIndex =  - (2 * visibleCount - 1);
    }
    lastIndex = thisStartIndex;
     = thisStartIndex;
}
const onScrollToBottom = () =&gt; {
    const scrollTop = ;
    const clientHeight = ;
    const scrollHeight = ;
    if (scrollTop + clientHeight &gt;= scrollHeight) {
        loadMore();
    }
}
let loadingLock = false;
let lockLoadMoreByHideLoading_once = false;
const loadMore = (async () =&gt; {
    if (loadingLock) return;
    if (lockLoadMoreByHideLoading_once) {
        lockLoadMoreByHideLoading_once = false;
        return;
    }
    loadingLock = true;
     = true;
    const moreData = await ().catch(err =&gt; {
        (err);
        ElMessage({
            message: 'Failed to obtain data, please check the network and try again',
            type: 'error',
        })
        return []
    })
    if ( != 0) {
         = [..., ...moreData];
        handleScroll();  
    }
     = false;
    lockLoadMoreByHideLoading_once = true;
    loadingLock = false;
})
function rafThrottle(fn) {
    let lock = false;
    return function (...args) {
        if (lock) return;
        lock = true;
        (() =&gt; {
            (args);
            lock = false;
        });
    };
}
onMounted(() =&gt; {
    ('scroll', handleScroll);
});
onUnmounted(() =&gt; {
    ('scroll', handleScroll);
});
&lt;/script&gt;
&lt;style scoped&gt;
.virtual-list {
    position: relative;
}
.scroll-box {
    overflow-y: auto;
}
&lt;/style&gt;

<template>
    <div class="loading" v-show="">
        <p>Loading...</p>
    </div>
</template>
<script setup>
    const pros = defineProps(['isShow'])
</script>
<style>
.loading {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 50px;
  background-color: #f5f5f5;
}
</style>

The following is the demo

&lt;script setup&gt;
import VirtualList from '@/components/';
import { onMounted, ref } from 'vue';
let listData = ref([]);
let num = 200;
// Simulate to get data from the APIconst fetchData = async () =&gt; {
  // This can be replaced with your actual API request  await new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; {
      const newData = ({ length: 200 }, (_, index) =&gt; `Item ${index}`);
       = newData;
      resolve();
    }, 500);
  })
}
// Load more dataconst loadMore = () =&gt; {
  // This can be replaced with your actual API request  return new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      const moreData = ({ length: 30 }, (_, index) =&gt; `Item ${num + index}`);
      num += 30;
      resolve(moreData);
    }, 500);
  });
  //Simulation request error  // return new Promise((_, reject) =&gt; {
  //   setTimeout(() =&gt; {
  // reject('Error simulation');  //   }, 1000);
  // })
}
onMounted(() =&gt; {
   fetchData();
});
&lt;/script&gt;
&lt;template&gt;
  &lt;!-- class="virtualContainer" --&gt;
  &lt;VirtualList v-if=" &gt; 0"
     :listData="listData" :itemHeight="50" :scrollHeight="600" :loadMore="loadMore"&gt;
      &lt;template #item="{ item,index }"&gt;
        &lt;div class="list-item"&gt;
          {{  item }}
        &lt;/div&gt;
      &lt;/template&gt;
  &lt;/VirtualList&gt;
&lt;/template&gt;
&lt;style scoped&gt;
.list-item {
  height: 50px;
  line-height: 50px;
  border-bottom: 1px solid #ccc;
  text-align: center;
}
&lt;/style&gt;

Here we add more commonly used loading, loading and error prompts, which will be more comprehensive, and overall it should be quite satisfying a lot of practicality.

This is the end of this article about the detailed explanation of the implementation of virtual list components in vue3. For more related contents of virtual list components in vue3, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!