vue簡單實現(xiàn)一個虛擬列表的示例代碼
一、前言
當(dāng)今的時代是大數(shù)據(jù)時代,往往一個列表就有成千上萬條數(shù)據(jù),而我們一一渲染的話,則需要耗費(fèi)大量時間,導(dǎo)致網(wǎng)頁打開緩慢。懶加載雖然減少了第一次渲染時間,加快了網(wǎng)頁打開速度,但隨著后續(xù)數(shù)據(jù)的不斷載入拼接,列表的渲染時間也會越來越長。虛擬列表則很好的解決了這一問題。
虛擬列表只渲染當(dāng)前可視區(qū)域的列表,并不會將所有的數(shù)據(jù)渲染。下面用Vue簡單實現(xiàn)移動端虛擬列表(并且支持下拉觸底加載效果)
二、代碼實現(xiàn)
準(zhǔn)備下拉數(shù)據(jù):
export default { data() { return { listData: [], // 總數(shù)據(jù) isLoading: false, // 展示loading }; }, mounted() { this.getListData(); }, methods: { // 獲取數(shù)據(jù) getListData() { const count = 20 + this.listData.length; const start = this.listData.length; this.isLoading = true; setTimeout(() => { for (let i = start; i < count; i++) { this.listData.push(i); } this.isLoading = false; }, 500); }, }, };
需要準(zhǔn)備內(nèi)外兩個列表容器,外部容器(viewport)固定高度用于生成滾動條,內(nèi)部容器(scrollbar)用于撐開外部容器使得滾動條保持與未使用虛擬列表時一致。
<template> <div class="viewport" ref="viewport"> <!-- 滾動條 --> <div class="scrollbar" :style="{ height: listHeight + 'px' }"></div> <!-- 展示的列表 --> <div class="list" :style="{ transform: `translateY(${transformOffset}px)` }" > <div class="row" :style="{ height: rowHeight + 'px' }" v-for="(item, index) in showList" :key="index" > <slot :record="item"></slot> </div> </div> <!-- 加載 --> <div class="loading_wrap" v-show="loading"> <div class="loading"> <div class="container"></div> </div> <div>正在加載中</div> </div> </div> </template> <style lang="less" scoped> /* ------最外層容器---------*/ .viewport { width: 100%; height: 100%; // 這個的高度讓父組件去決定 background-color: #fff; position: relative; overflow-y: auto; } /* ------列表展示層容器---------*/ .list { position: absolute; top: 0; left: 0; right: 0; } /* ------每行容器---------*/ .row { overflow: hidden; } </style>
計算列表的總高度listHeight(列表的總條數(shù)乘以每一條的高度),可視區(qū)域的高度viewHeight。
計算當(dāng)前可見區(qū)域起始數(shù)據(jù)的startIndex和結(jié)束數(shù)據(jù)的endIndex,監(jiān)聽viewport列表滾動事件,計算currentIndex以及列表的偏移量transformOffset。
監(jiān)聽滾動事件動態(tài)設(shè)置顯示的列表(showList)。
<script lang="ts" setup> import { defineProps, withDefaults, defineEmits, ref, onMounted, computed, } from "vue"; interface Props { list: string[]; // 數(shù)據(jù)源 rowHeight: number; // 每行的高度 viewCount: number; // 顯示數(shù)量 loading: boolean; // 控制loading } const props = withDefaults(defineProps<Props>(), { list: () => [], rowHeight: 200, viewCount: 10, loading: false, }); const emit = defineEmits<{ (e: "bottomLoad"): void; }>(); let viewHeight = ref(0); //可視區(qū)域的高度 let startIndex = ref(0); //開始索引 let endIndex = ref(0); //結(jié)束索引 let transformOffset = ref(0); //列表的偏移量 const viewport = ref(null); onMounted(() => { initData(); }); let showList = computed(() => props.list.slice(startIndex.value, endIndex.value) ); //展示的數(shù)據(jù) let listHeight = computed(() => props.list.length * props.rowHeight); //列表的總高度 // 初始化一些數(shù)據(jù) const initData = () => { endIndex.value = props.viewCount; viewHeight.value = viewport.value.offsetHeight; }; // 列表滾動 const onScroll = () => { const scrollTop = viewport.value.scrollTop; // 獲取試圖往上滾動的高度 const currentIndex = Math.floor(scrollTop / props.rowHeight); // 計算當(dāng)前的索引 // 只在需要變化的時 才重新賦值 if (startIndex.value !== currentIndex) { startIndex.value = currentIndex; endIndex.value = startIndex.value + props.viewCount; // 結(jié)束索引 transformOffset.value = scrollTop - (scrollTop % props.rowHeight); } // 觸底了 if (Math.round(viewHeight.value + scrollTop) === listHeight.value) { // 發(fā)送觸底加載事件 emit("bottomLoad"); } }; </script>
三、完整代碼
demo.vue
<template> <div class="page"> <h3>長列表渲染</h3> <ListScroll class="list_scroll" :list="listData" :loading="isLoading" @bottomLoad="onBottomLoad" > <template v-slot="{ record }"> <div class="row_content" @click="handleClick(record)"> <div>{{ record }}</div> <img class="image" src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F2076f7ae-d134-4dc4-a865-af1b2029d400%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1680249943&t=7646a71b62c810256a2b414e96106808" /> </div> </template> </ListScroll> </div> </template> <script> export default { data() { return { listData: [], // 總數(shù)據(jù) isLoading: false, // 展示loading }; }, mounted() { this.getListData(); }, methods: { // 獲取數(shù)據(jù) getListData() { const count = 20 + this.listData.length; const start = this.listData.length; this.isLoading = true; setTimeout(() => { for (let i = start; i < count; i++) { this.listData.push(i); } this.isLoading = false; }, 500); }, // 監(jiān)聽觸底事件 onBottomLoad() { console.log("觸底了"); if (this.listData.length >= 100) { console.log("數(shù)據(jù)加載完了~"); return; } // 加載數(shù)據(jù) this.getListData(); }, // 監(jiān)聽點擊每行 handleClick(record) { console.log(record, "record"); }, }, }; </script> <style lang="less" scoped> .page { display: flex; flex-direction: column; height: 100vh; .list_scroll { flex: 1; } } .row_content { width: 100%; height: 100%; .image { display: block; width: 100%; height: 160px; object-fit: cover; } } </style>
ListScroll.vue
<template> <div class="viewport" ref="viewport" @scroll="onScroll"> <!-- 滾動條 --> <div class="scrollbar" :style="{ height: listHeight + 'px' }"></div> <!-- 展示的列表 --> <div class="list" :style="{ transform: `translateY(${transformOffset}px)` }" > <div class="row" :style="{ height: rowHeight + 'px' }" v-for="(item, index) in showList" :key="index" > <slot :record="item"></slot> </div> </div> <!-- 加載 --> <div class="loading_wrap" v-show="loading"> <div class="loading"> <div class="container"></div> </div> <div>正在加載中</div> </div> </div> </template> <script lang="ts" setup> import { defineProps, withDefaults, defineEmits, ref, onMounted, computed, } from "vue"; interface Props { list: string[]; // 數(shù)據(jù)源 rowHeight: number; // 每行的高度 viewCount: number; // 顯示數(shù)量 loading: boolean; // 控制loading } const props = withDefaults(defineProps<Props>(), { list: () => [], rowHeight: 200, viewCount: 10, loading: false, }); const emit = defineEmits<{ (e: "bottomLoad"): void; }>(); let viewHeight = ref(0); //可視區(qū)域的高度 let startIndex = ref(0); //開始索引 let endIndex = ref(0); //結(jié)束索引 let transformOffset = ref(0); //列表的偏移量 const viewport = ref(null); onMounted(() => { initData(); }); let showList = computed(() => props.list.slice(startIndex.value, endIndex.value) ); //展示的數(shù)據(jù) let listHeight = computed(() => props.list.length * props.rowHeight); //列表的總高度 // 初始化一些數(shù)據(jù) const initData = () => { endIndex.value = props.viewCount; viewHeight.value = viewport.value.offsetHeight; }; // 列表滾動 const onScroll = () => { const scrollTop = viewport.value.scrollTop; // 獲取試圖往上滾動的高度 const currentIndex = Math.floor(scrollTop / props.rowHeight); // 計算當(dāng)前的索引 // 只在需要變化的時 才重新賦值 if (startIndex.value !== currentIndex) { startIndex.value = currentIndex; endIndex.value = startIndex.value + props.viewCount; // 結(jié)束索引 transformOffset.value = scrollTop - (scrollTop % props.rowHeight); } // 觸底了 if (Math.round(viewHeight.value + scrollTop) === listHeight.value) { // 發(fā)送觸底加載事件 emit("bottomLoad"); } }; </script> <style lang="less" scoped> /* ------最外層容器---------*/ .viewport { width: 100%; height: 100%; // 這個的高度讓父組件去決定 background-color: #fff; position: relative; overflow-y: auto; } /* ------列表展示層容器---------*/ .list { position: absolute; top: 0; left: 0; right: 0; } /* ------每行容器---------*/ .row { overflow: hidden; } /* ------loading樣式---------*/ .loading_wrap { display: flex; justify-content: center; align-items: center; color: #999; padding: 20px 0; .loading { box-sizing: border-box; width: 20px; height: 20px; border: 2px solid #ddd; border-radius: 50%; animation: rotate 1s linear infinite; margin-right: 10px; } .container { position: relative; top: 50%; left: 50%; width: 10px; height: 10px; background-color: #fff; } } /* ------loading動畫---------*/ @keyframes rotate { from { transform-origin: center center; transform: rotate(0deg); } to { transform-origin: center center; transform: rotate(360deg); } } </style>
四、實現(xiàn)效果
五、實現(xiàn)效果
實現(xiàn)虛擬列表就是處理滾動條滾動后的可見區(qū)域的變更,具體實現(xiàn)步驟如下:
- 計算當(dāng)前可見區(qū)域起始數(shù)據(jù)的startIndex
- 計算當(dāng)前可見區(qū)域借宿數(shù)據(jù)的endIndex
- 計算當(dāng)前可見區(qū)域的數(shù)據(jù),并渲染到頁面中
- 計算startIndex對應(yīng)的數(shù)據(jù)在整個列表中的偏移位置transformOffset,并設(shè)置到列表上
到此這篇關(guān)于vue簡單實現(xiàn)一個虛擬列表的示例代碼的文章就介紹到這了,更多相關(guān)vue 虛擬列表內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue中調(diào)接口的方式詳解this.$api、直接調(diào)用、axios
這篇文章主要介紹了vue中調(diào)接口的方式:this.$api、直接調(diào)用、axios,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-11-11Vue 實現(xiàn)點擊空白處隱藏某節(jié)點的三種方式(指令、普通、遮罩)
最近小編接到這樣的需求:彈出框(或Popover)在 show 后,點擊空白處可以將其 hide。針對這個需求,小編整理了三種實現(xiàn)方式,如果大家對vue 點擊空白隱藏節(jié)點問題感興趣的朋友跟隨小編一起看看吧2019-10-10element-ui中el-input只輸入數(shù)字(包括整數(shù)和小數(shù))
開發(fā)中有時候需要input只能輸入數(shù)字,下面這篇文章主要給大家介紹了關(guān)于element-ui中el-input只輸入數(shù)字(包括整數(shù)和小數(shù))的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09Vue 清除Form表單校驗信息的解決方法(清除表單驗證上次提示信息)
這篇文章主要介紹了Vue 清除Form表單校驗信息的解決方法(清除表單驗證上次提示信息),本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04