Vue3實(shí)現(xiàn)一個(gè)定高的虛擬列表
前言
虛擬列表對于大部分一線開發(fā)同學(xué)來說是一點(diǎn)都不陌生的東西了,有的同學(xué)是直接使用第三方組件。但是面試時(shí)如果你簡歷上面寫了虛擬列表,卻給面試官說是通過三方組件實(shí)現(xiàn)的,此時(shí)空氣可能都凝固了。所以這篇文章歐陽將會教你2分鐘內(nèi)實(shí)現(xiàn)一個(gè)定高的虛擬列表,至于不定高的虛擬列表下一篇文章來寫。
什么是虛擬列表
有的特殊場景我們不能分頁,只能渲染一個(gè)長列表。這個(gè)長列表中可能有幾萬條數(shù)據(jù),如果全部渲染到頁面上用戶的設(shè)備差點(diǎn)可能就會直接卡死了,這時(shí)我們就需要虛擬列表來解決問題。
一個(gè)常見的虛擬列表是下面這樣的,如下圖:
其中實(shí)線框的item表示在視口區(qū)域內(nèi)真實(shí)渲染DOM,虛線框的item表示并沒有渲染的DOM。
在定高的虛擬列表中,我們可以根據(jù)可視區(qū)域的高度
和每個(gè)item的高度
計(jì)算得出在可視區(qū)域內(nèi)可以渲染多少個(gè)item。不在可視區(qū)域里面的item那么就不需要渲染了(不管有幾萬個(gè)還是幾十萬個(gè)item),這樣就能解決長列表性能很差的問題啦。
實(shí)現(xiàn)滾動條
按照上面的圖,很容易想到我們的dom結(jié)構(gòu)應(yīng)該是下面這樣的:
<template> <div class="container"> <div class="list-wrapper"> <!-- 只渲染可視區(qū)域列表數(shù)據(jù) --> </div> </div> </template> <style scoped> .container { height: 100%; overflow: auto; position: relative; } </style>
給可視區(qū)域container
設(shè)置高度100%
,也可以是一個(gè)固定高度值。并且設(shè)置overflow: auto;
讓內(nèi)容在可視區(qū)域中滾動。
此時(shí)我們遇見第一個(gè)問題,滾動條是怎么來的,可視區(qū)域是靠什么撐開的?
答案很簡單,我們知道每個(gè)item的高度itemSize
,并且知道有多少條數(shù)據(jù)listData.length
。那么itemSize * listData.length
不就是真實(shí)的列表高度了嗎。所以我們可以在可視區(qū)域container
中新建一個(gè)名為placeholder
的空div,將他的高度設(shè)置為itemSize * listData.length
,這樣可視區(qū)域就被撐開了,并且滾動條也有了。代碼如下:
<template> <div class="container"> <div class="placeholder" :style="{ height: listHeight + 'px' }"></div> <div class="list-wrapper"> <!-- 只渲染可視區(qū)域列表數(shù)據(jù) --> </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(() => listData.length * itemSize); </script> <style scoped> .container { height: 100%; overflow: auto; position: relative; } .placeholder { position: absolute; left: 0; top: 0; right: 0; z-index: -1; } </style>
placeholder
采用絕對定位,為了不擋住可視區(qū)域內(nèi)渲染的列表,所以將其設(shè)置為z-index: -1
。
接下來就是計(jì)算容器里面到底渲染多少個(gè)item,很簡單,Math.ceil(可視區(qū)域的高度 / 每個(gè)item的高度)
。
為什么使用Math.ceil
向上取整呢?
只要有個(gè)item在可視區(qū)域漏了一點(diǎn)出來,我們也應(yīng)該將其渲染。
此時(shí)我們就能得到幾個(gè)變量:
start
:可視區(qū)域內(nèi)渲染的第一個(gè)item的index的值,初始化為0。renderCount
:可視區(qū)域內(nèi)渲染的item數(shù)量。end
:可視區(qū)域內(nèi)渲染的最后一個(gè)item的index值,他的值等于start + renderCount
。注意我們這里使用start + renderCount
實(shí)際是多渲染了一個(gè)item,比如start = 0
和renderCount = 2
,我們設(shè)置的是end = 2
,實(shí)際是渲染了3個(gè)item。目的是為了預(yù)渲染下一個(gè),后面會講。
監(jiān)聽滾動事件
有了滾動條后就可以開始滾動了,我們監(jiān)聽container
容器的scroll事件。
可視區(qū)域中的內(nèi)容應(yīng)該隨著滾動條的滾動而變化,也就是說在scroll事件中我們需要重新計(jì)算start
的值。
function handleScroll(e) { const scrollTop = e.target.scrollTop; start.value = Math.floor(scrollTop / itemSize); offset.value = scrollTop - (scrollTop % itemSize); }
如果當(dāng)前itemSize
的值為100。
如果此時(shí)滾動的距離在0-100之間,比如下面這樣:
上面這張圖item1還沒完全滾出可視區(qū)域,有部分在可視區(qū)域內(nèi),部分在可視區(qū)域外。此時(shí)可視區(qū)域內(nèi)顯示的就是item1-item7
的模塊了,這就是為什么前面我們計(jì)算end時(shí)要多渲染一個(gè)item,不然這里item7就沒法顯示了。
滾動距離在0-100之間時(shí),渲染的DOM沒有變化,我們完全是復(fù)用瀏覽器的滾動,并沒有進(jìn)行任何處理。
當(dāng)scrollTop
的值為100時(shí),也就是剛剛把item1滾到可視區(qū)外面時(shí)。此時(shí)item1已經(jīng)不需要渲染了,因?yàn)橐呀?jīng)看不見他了。所以此時(shí)的start
的值就應(yīng)該從0
更新為1
,同理如果scrollTop
的值為110
,start的值也一樣是1
。所以得出start.value = Math.floor(scrollTop / itemSize);
如下圖:
此時(shí)的start
從item2開始渲染,但是由于前面我們復(fù)用了瀏覽器的滾動,所以實(shí)際渲染的DOM第一個(gè)已經(jīng)在可視區(qū)外面了。此時(shí)可視區(qū)看見的第一個(gè)是item3,很明顯是不對的,應(yīng)該看見的是第一個(gè)是item2。
此時(shí)應(yīng)該怎么辦呢?
很簡單,使用translate
將列表向下偏移一個(gè)item的高度就行,也就是100px。列表偏移后就是下面這樣的了:
如果當(dāng)前scrollTop
的值為200,那么偏移值就是200px。所以我們得出
offset.value = scrollTop - (scrollTop % itemSize);
為什么這里要減去scrollTop % itemSize
呢?
因?yàn)樵跐L動時(shí)如果是在item的高度范圍內(nèi)滾動,我們是復(fù)用瀏覽器的滾動,此時(shí)無需進(jìn)行偏移,所以計(jì)算偏移值時(shí)需要減去scrollTop % itemSize
。
實(shí)際上從一個(gè)item滾動到另外一個(gè)item時(shí),比如從item0
滾動到item1
。此時(shí)會做兩件事情:將start
的值從0
更新為1
和根據(jù)scrollTop
計(jì)算得到列表的偏移值100
,從而讓新的start對應(yīng)的item1
重新回到可視范圍內(nèi)。
這個(gè)是運(yùn)行效果圖:
下面是完整的代碼:
<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="item.id" :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px', backgroundColor: `rgba(0,0,0,${item.value / 100})`, }" > {{ item.value + 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(() => Math.ceil(containerHeight.value / itemSize)); const start = ref(0); const offset = ref(0); const end = computed(() => start.value + renderCount.value); const listHeight = computed(() => listData.length * itemSize); const renderList = computed(() => listData.slice(start.value, end.value + 1)); const getTransform = computed(() => `translate3d(0,${offset.value}px,0)`); onMounted(() => { containerHeight.value = container.value.clientHeight; }); function handleScroll(e) { const scrollTop = e.target.scrollTop; start.value = Math.floor(scrollTop / itemSize); offset.value = 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>
這個(gè)是父組件的代碼:
<template> <div style="height: 100vh; width: 100vw"> <VirtualList :listData="data" :itemSize="100" /> </div> </template> <script setup> import VirtualList from "./common.vue"; import { ref } from "vue"; const data = ref([]); for (let i = 0; i < 1000; i++) { data.value.push({ id: i, value: i }); } </script> <style> html { height: 100%; } body { height: 100%; margin: 0; } #app { height: 100%; } </style>
總結(jié)
這篇文章我們講了如何實(shí)現(xiàn)一個(gè)定高的虛擬列表,首先根據(jù)可視區(qū)域的高度和item的高度計(jì)算出視口內(nèi)可以渲染出來的item數(shù)量renderCount
。然后根據(jù)滾動的距離去計(jì)算start
的位置,計(jì)算end
的位置時(shí)使用start + renderCount
預(yù)渲染一個(gè)item。在每個(gè)item范圍內(nèi)滾動時(shí)直接復(fù)用瀏覽器的滾動,此時(shí)無需進(jìn)行任何處理。當(dāng)從一個(gè)item滾動到另外一個(gè)item時(shí),此時(shí)會做兩件事情:更新start的值和根據(jù)scrollTop
計(jì)算列表的偏移值讓新的start對應(yīng)的item重新回到可視范圍內(nèi)。
以上就是Vue3實(shí)現(xiàn)一個(gè)定高的虛擬列表的詳細(xì)內(nèi)容,更多關(guān)于Vue3定高虛擬列表的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue發(fā)送websocket請求和http post請求的實(shí)例代碼
這篇文章主要介紹了vue發(fā)送websocket請求和http post請求的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-07-07詳解如何制作并發(fā)布一個(gè)vue的組件的npm包
這篇文章主要介紹了詳解如何制作并發(fā)布一個(gè)vue的組件的npm包,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11vue復(fù)制內(nèi)容到剪切板代碼實(shí)現(xiàn)
這篇文章主要給大家介紹了關(guān)于vue復(fù)制內(nèi)容到剪切板代碼實(shí)現(xiàn)的相關(guān)資料,在Web應(yīng)用程序中剪貼板(Clipboard)操作是非常常見的操作之一,需要的朋友可以參考下2023-08-08vue vantUI實(shí)現(xiàn)文件(圖片、文檔、視頻、音頻)上傳(多文件)
這篇文章主要介紹了vue vantUI實(shí)現(xiàn)文件(圖片、文檔、視頻、音頻)上傳(多文件),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10