前端虛擬滾動列表實現(xiàn)代碼(vue虛擬列表)
前言
方法一利用瀏覽器原生api去實現(xiàn),可以實現(xiàn)不等高的列表虛擬滾動,intersectionObserver 多用于圖片懶加載,虛擬滾動列表
方法二通過監(jiān)聽滾動條的位置,去計算顯示的內容,這里需要列表等高,當然不等高也可以計算,稍微改改
前端虛擬滾動列表(方法一:利用IntersectionObserver api 簡單)
- IntersectionObserver可以用來自動監(jiān)聽元素是否進入了設備的可視區(qū)域之內,而不需要頻繁的計算來做這個判斷。由于可見(visible)的本質是,目標元素與視口產(chǎn)生一個交叉區(qū),所以這個 API 叫做"交叉觀察器"IntersectionObserver 方案多用于圖片懶加載或者列表虛擬滾動
IntersectionObserver 是瀏覽器原生提供的構造函數(shù),接受兩個參數(shù): callback:可見性發(fā)現(xiàn)變化時的回調函數(shù) option:配置對象(可選)。構造函數(shù)的返回值是一個觀察器實例。實例一共有4個方法:
observe:開始監(jiān)聽特定元素
unobserve:停止監(jiān)聽特定元素
disconnect:關閉監(jiān)聽工作
takeRecords:返回所有觀察目標的對象數(shù)組
callback 參數(shù)
目標元素的可見性變化時,就會調用觀察器的回調函數(shù)callback。
callback一般會觸發(fā)兩次。一次是目標元素剛剛進入視口,另一次是完全離開視口。
const io = new IntersectionObserver((changes, observer) => { console.log(changes); console.log(observer); });
- options
- threshold: 決定了什么時候觸發(fā)回調函數(shù)。它是一個數(shù)組,每個成員都是一個門檻值,默認為[0],即交叉比例(intersectionRatio)達到0時觸發(fā)回調函數(shù)。用戶可以自定義這個數(shù)組。比如,[0, 0.25, 0.5, 0.75, 1]就表示當目標元素 0%、25%、50%、75%、100% 可見時,會觸發(fā)回調函數(shù)。
- root: 用于觀察的根元素,默認是瀏覽器的視口,也可以指定具體元素,指定元素的時候用于觀察的元素必須是指定元素的子元素
- rootMargin: 用來擴大或者縮小視窗的的大小,使用css的定義方法,10px 10px 30px 20px表示top、right、bottom 和 left的值
————————————————
這里是后面補充的簡單還原了下面方法二的例子,重點在60行,從哪兒看就可以
<template> <div class="big-box"> <div class="download-box txt" id="scrollable-div"> <div v-for="(item, index) in props.seqText" :key="index" class="line-box"> <template v-if="index === 0 && start === 0"> <div :class="{ 'text-title': props.collapsed, 'text-title-samll': !props.collapsed }"> {{ item }} </div> </template> <template v-else> <div :class="{ 'text-number': props.collapsed, 'text-number-samll': !props.collapsed }"> {{ calLine(item, index + start) }} </div> <div :class="{ 'text-box': props.collapsed, 'text-box-samll': !props.collapsed }" :data="item" > '' </div> <div :class="{ 'text-number2': props.collapsed, 'text-number2-samll': !props.collapsed }"> {{ endRow(item, index + start) }} </div> </template> </div> </div> </div> <SearchBox :againFind="againFind" /> </template> <script lang="ts" setup> import { watch, onMounted, PropType, reactive, ref } from 'vue'; import SearchBox from '/@/components/SearchBox/index.vue'; import { message } from 'ant-design-vue'; const props = defineProps({ collapsed: { type: Boolean, default: true, }, seqText: { type: Array as PropType<string[]>, default: [''], }, }); let width = 100; const geneTexts: Array<string> = []; const data = reactive({ geneTexts, }); const calLine = (item: any, index: number) => { return width * (index - 1) + 1; }; const endRow = (item: any, index: number) => { return width * index; }; // 這里是核心要點 const io = new IntersectionObserver( (entries) => { console.log(entries); for (const entry of entries) { if (entry.isIntersecting) { const elTxt = entry.target; // console.log(elTxt.getAttribute('data')); elTxt.innerHTML = elTxt.getAttribute('data'); io.unobserve(elTxt); } } }, { root: document.getElementById('scrollable-div'), // rootMargin: 0, threshold: 0.5, }, ); setTimeout(() => { const elList = document.querySelectorAll('.text-box'); console.log(elList); elList.forEach((element) => { io.observe(element); }); }, 1000); const againFind = ref(1); let start = ref(0); </script> <style lang="less" scoped> // @import '/@/assets/styles/views/medaka.less'; .big-box { background: #282c34; padding: 30px 20px; height: 870px; } .download-box { width: 100%; // padding: 0px 20px; // outline: 1px solid rgb(17, 0, 255); overflow: hidden; .line-box { .flex-type(flex-start); height: 30px; } &.txt { background: #282c34; color: #fff; height: 810px; overflow: auto; .el-row { display: flex; align-items: center; margin-bottom: 10px; margin: auto; font-size: 22px; } } } @media screen and (min-width: 1842px) { .text-box-samll { letter-spacing: 1.5px; font-size: 15px; } .text-number-samll { min-width: 60px; font-size: 15px; } .text-number2-samll { margin-left: 20px; min-width: 60px; font-size: 15px; } .text-title-samll { font-size: 15px; } .text-box { font-size: 22px; // letter-spacing: 3px; } .text-number { min-width: 100px; font-size: 22px; } .text-number2 { margin-left: 20px; min-width: 100px; font-size: 22px; } .text-title { font-size: 22px; } } @media screen and (min-width: 1600px) and (max-width: 1841px) { .text-box-samll { font-size: 15px; } .text-number-samll { min-width: 40px; font-size: 15px; } .text-number2-samll { margin-left: 20px; min-width: 40px; font-size: 15px; } .text-title-samll { font-size: 15px; } .text-box { font-size: 20px; // letter-spacing: 1.2px; } .text-number { min-width: 60px; font-size: 20px; } .text-number2 { margin-left: 20px; min-width: 60px; font-size: 20px; } .text-title { font-size: 20px; } } @media screen and (min-width: 1443px) and (max-width: 1599px) { .text-box-samll { font-size: 13px; } .text-number-samll { min-width: 40px; font-size: 13px; } .text-number2-samll { margin-left: 20px; min-width: 40px; font-size: 13px; } .text-title-samll { font-size: 13px; } .text-box { font-size: 18px; // letter-spacing: 1.2px; } .text-number { min-width: 60px; font-size: 15px; } .text-number2 { margin-left: 20px; min-width: 60px; font-size: 18px; } .text-title { font-size: 18px; } } @media screen and (max-width: 1442px) { .text-box-samll { font-size: 11px; } .text-number-samll { min-width: 40px; font-size: 11px; } .text-number2-samll { margin-left: 20px; min-width: 40px; font-size: 11px; } .text-title-samll { font-size: 11px; } .text-box { font-size: 16px; // letter-spacing: 1.2px; } .text-number { min-width: 60px; font-size: 15px; } .text-number2 { margin-left: 20px; min-width: 60px; font-size: 16px; } .text-title { font-size: 16px; } } </style>
前端虛擬滾動列表(方法二:監(jiān)聽滾動計算 麻煩)
在大型的企業(yè)級項目中經(jīng)常要渲染大量的數(shù)據(jù),這種長列表是一個很普遍的場景,當列表內容越來越多就會導致頁面滑動卡頓、白屏、數(shù)據(jù)渲染較慢的問題;大數(shù)據(jù)量列表性能優(yōu)化,減少真實dom的渲染
看圖:綠色是顯示區(qū)域,綠色和藍色中間屬于預加載:解決滾動閃屏問題;大致了解了流程在往下看;
實現(xiàn)效果:
先說一下你看到這么多真實dom節(jié)點是因為做了預加載,減少滾動閃屏現(xiàn)象,這里寫了300行,可以根據(jù)實際情況進行截取
實現(xiàn)思路:
虛擬列表滾動大致思路:兩個div容器
外層:外部容器用來固定列表容器的高度,同時生成滾動條
內層:內部容器用來裝元素,高度是所有元素高度的和
外層容器鼠標滾動事件 dom.scrollTop 獲取滾動條的位置
根據(jù)每行列表的高以及當前滾動條的位置,利用slice() 去截取當前需要顯示的內容
重點:滾動條的高度是有內層容器的paddingBottom 和 paddingTop 屬性頂起來了,確保滾動條位置的準確性
這里鼠標上下滾動會出現(xiàn)閃屏問題:解決方案如下:
方案一: 預加載:
向下預加載:
比如div滾動區(qū)域顯示30行,就預加載 300行( 即這里 slice(startIndex,startIndex + 300) ),向上預加載:
在滾動監(jiān)聽事件函數(shù)中(computeRow)判斷inner的paddingTop和paddingBottom即可當然這里的download-box的padding有30px像素,在加一個div,overflow:hidded就解決了
方案二:縮小滾動范圍或者節(jié)流時間縮短,這里寫的500ms
具體代碼
<template> <div class="enn"> <div class="download-box txt" id="scrollable-div" @scroll="handleScroll"> <div id="inner"> <div v-for="(item, index) in data2" :key="index" class="line-box"> <div :class="{ 'text-box': props.collapsed, 'text-box-samll': !props.collapsed }"> {{ item }} </div> </div> </div> </div> </div> </template> <script lang="ts" setup> import { onMounted, PropType, ref } from 'vue'; import { useText } from './hooks/useText'; const props = defineProps({ baseData: { type: Object as PropType<{ taskId: string; barcodeName: string; }>, default: {}, }, collapsed: { type: Boolean, default: true, }, type: { type: Boolean, default: false, }, }); const { data } = useText(props.type); // 這里大數(shù)據(jù)量數(shù)組是 data.geneTexts /** * 虛擬列表滾動大致思路:兩個div容器 * * 外層:外部容器用來固定列表容器的高度,同時生成滾動條 * * 內層:內部容器用來裝元素,高度是所有元素高度的和 * * 外層容器鼠標滾動事件 dom.scrollTop 獲取滾動條的位置 * * 根據(jù)每行列表的高以及當前滾動條的位置,利用slice() 去截取當前需要顯示的內容 * * 重點:滾動條的高度是有內層容器的paddingBottom 和 paddingTop 屬性頂起來了,確保滾動條位置的準確性 * * 這里鼠標上下滾動會出現(xiàn)閃屏問題:解決方案如下: * * 方案一: 預加載: * * 向下預加載: * 比如div滾動區(qū)域顯示30行,就預加載 300行( 即這里 slice(startIndex,startIndex + 300) ), * * 向上預加載: * 在滾動監(jiān)聽事件函數(shù)中(computeRow)判斷inner的paddingTop和paddingBottom即可 * * 當然這里的download-box的padding有30px像素,在加一個div,overflow:hidded就解決了 * * 方案二:縮小滾動范圍或者節(jié)流時間縮短,這里寫的500ms * * */ let timer_throttle: any; const throttle = (func: Function, wait?: number) => { wait = wait || 500; if (!timer_throttle) { timer_throttle = setTimeout(() => { func.apply(this); timer_throttle = null; }, wait); } }; // 鼠標滾動事件 const handleScroll = (event: any) => throttle(computeRow, 100); // 計算當前顯示tab const computeRow = () => { // console.log('距離頂部距離', window.scrollY, geneTexts); let scrollableDiv = document.getElementById('scrollable-div'); let topPosition = scrollableDiv.scrollTop; let leftPosition = scrollableDiv.scrollLeft; console.log('垂直滾動位置:', topPosition, '水平滾動位置:', leftPosition); const startIndex = Math.max(0, Math.floor(topPosition / 30)); const endIndex = startIndex + 300; data2.value = data.geneTexts.slice(startIndex, endIndex); let inner = document.getElementById('inner'); if (topPosition < 2700) { // 向上預計加載,這里判斷了三個高度,可以多判斷幾個,增加流暢度 inner.style.paddingTop = topPosition + 'px'; inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 - topPosition + 'px'; } else if (topPosition + data2.value.length * 30 >= data.geneTexts.length * 30) { // 這里 9000 是 內層div的高度 30 * 300 理解div高度是 padding+div內容高度 inner.style.paddingTop = topPosition - 900 + 'px'; //900 是div的高度 inner.style.paddingBottom = 0 + 'px'; } else { inner.style.paddingTop = topPosition - 2700 + 'px'; inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 + 2700 - topPosition + 'px'; } }; const data2 = ref([]); const init = () => { data2.value = data.geneTexts.slice(0, 300); let inner = document.getElementById('inner'); inner.style.paddingTop = 0 + 'px'; inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 - 900 + 'px'; }; </script> <style lang="less" scoped> .button-box { margin-bottom: 25px; .flex-type(flex-end); :deep(.ant-btn) { margin-left: 10px; } } .enn { background: #282c34; outline: 1px solid red; padding: 30px 20px; height: 960px; } .download-box { width: 100%; // padding: 30px 20px; outline: 1px solid rgb(17, 0, 255); background-color: #fff; overflow: hidden; .line-box { .flex-type(flex-start); height: 30px; } &.txt { background: #282c34; color: #fff; height: 900px; overflow: auto; } } </style>
替代方案
上面是自己寫的,github上面還有好多插件可以用,但各有優(yōu)劣,根據(jù)自己需求選擇
如:
https://github.com/Akryum/vue-virtual-scroller/tree/0f2e36248421ad69f41c9a08b8dcf7839527b8c2
vue-virt-list
vue-draggable-virtual-scroll-list
virtual-list
自己找吧,我就不一一列舉了,看圖
<template> <br /> <div> <Table :columns="tableConfig.columns" :data="tableConfig.totalData" :loading="tableConfig.loading" :pagination="false" ></Table> </div> <br /> <div class="button-box"> <a-select v-model:value="selection" placeholder="請選擇序列" :options="seqOptions" @change=" (selection:string) => handleChangeSeq(baseData.taskId, baseData.barcodeName, width, selection) " ></a-select> <a-button type="primary" @click="handleClickExport()">導出所有序列</a-button> <a-button type="primary" @click="modalConfig.visible = true">導出當前序列</a-button> </div> <!-- <SeqText :collapsed="props.collapsed" :seqText="data.geneTexts" /> --> <div class="enn"> <div class="download-box txt" id="scrollable-div" @scroll="handleScroll"> <div id="inner"> <div v-for="(item, index) in data2" :key="index" class="line-box"> <template v-if="index === 0 && start === 0"> <div :class="{ 'text-title': props.collapsed, 'text-title-samll': !props.collapsed }"> {{ item }} </div> </template> <template v-else> <div :class="{ 'text-number': props.collapsed, 'text-number-samll': !props.collapsed }"> {{ calLine(item, index + start) }} </div> <div :class="{ 'text-box': props.collapsed, 'text-box-samll': !props.collapsed }"> {{ item }} </div> <div :class="{ 'text-number2': props.collapsed, 'text-number2-samll': !props.collapsed }" > {{ endRow(item, index + start) }} </div> </template> </div> </div> </div> </div> <br /> <a-modal title="導出文件" :visible="modalConfig.visible" @ok="handleExport(data.geneTexts)" @cancel="modalConfig.visible = false" > <div class="form-box"> <a-form> <a-form-item label="自定義文件名"> <a-input v-model:value="modalConfig.name" placeholder="請輸入自定義文件名"></a-input> </a-form-item> </a-form> </div> </a-modal> </template> <script lang="ts" setup> import { defineComponent, onMounted, PropType, ref } from 'vue'; import Table from '/@/components/table/sTable.vue'; import SeqText from '/@/components/SeqText/index.vue'; import { useText, useTable } from './hooks/useText'; import { useModal } from './hooks/useModal'; import { serverAddress } from '/@/serve/index'; import { download, downloadTxt } from '/@/libs/utils/download'; const props = defineProps({ /** * 基礎數(shù)據(jù) */ baseData: { type: Object as PropType<{ taskId: string; barcodeName: string; }>, default: {}, }, collapsed: { type: Boolean, default: true, }, type: { type: Boolean, default: false, }, }); let width = 100; const { taskId, barcodeName } = props.baseData; const { data, getMedaka, getAvailableSeq, handleChangeSeq, seqOptions, selection } = useText( props.type, ); const { tableConfig, getTable } = useTable(props.type); const VITE_APP_URL = serverAddress(); const { modalConfig, handleExport } = useModal(); const handleClickExport = () => { let path = ''; if (props.type) { path = VITE_APP_URL + `outputs/${taskId}/fastq_analysis/${barcodeName}/ragtag.fasta`; } else { path = VITE_APP_URL + `outputs/${taskId}/fastq_analysis/${barcodeName}/${barcodeName}.final.fasta`; } download(path, '.fasta'); }; const calLine = (item: any, index: number) => { return width * (index - 1) + 1; }; const endRow = (item: any, index: number) => { return width * index; }; onMounted(() => { getAvailableSeq(taskId, barcodeName).then(() => { if (seqOptions.value.length > 0) { getMedaka(taskId, barcodeName, width, seqOptions.value[0].value).then(() => init()); // getMedaka(taskId, barcodeName, width); } }); getTable(taskId, barcodeName); }); /** * 虛擬列表滾動大致思路:兩個div容器 * * 外層:外部容器用來固定列表容器的高度,同時生成滾動條 * * 內層:內部容器用來裝元素,高度是所有元素高度的和 * * 外層容器鼠標滾動事件 dom.scrollTop 獲取滾動條的位置 * * 根據(jù)每行列表的高以及當前滾動條的位置,利用slice() 去截取當前需要顯示的內容 * * 重點:滾動條的高度是有內層容器的paddingBottom 和 paddingTop 屬性頂起來了,確保滾動條位置的準確性 * * 這里鼠標上下滾動會出現(xiàn)閃屏問題:解決方案如下: * * 方案一: 預加載: * * 向下預加載: * 比如div滾動區(qū)域顯示30行,就預加載 300行( 即這里 slice(startIndex,startIndex + 300) ), * * 向上預加載: * 在滾動監(jiān)聽事件函數(shù)中(computeRow)判斷inner的paddingTop和paddingBottom即可 * * 當然這里的download-box的padding有30px像素,在加一個div,overflow:hidded就解決了 * * 方案二:縮小滾動范圍或者節(jié)流時間縮短,這里寫的500ms * * */ let timer_throttle: any; const throttle = (func: Function, wait?: number) => { wait = wait || 500; if (!timer_throttle) { timer_throttle = setTimeout(() => { func.apply(this); timer_throttle = null; }, wait); } }; let start = ref(0); // 鼠標滾動事件 const handleScroll = (event: any) => throttle(computeRow, 100); // 計算當前顯示tab const computeRow = () => { // console.log('距離頂部距離', window.scrollY, geneTexts); let scrollableDiv = document.getElementById('scrollable-div'); let topPosition = scrollableDiv.scrollTop; let leftPosition = scrollableDiv.scrollLeft; console.log('垂直滾動位置:', topPosition, '水平滾動位置:', leftPosition); const startIndex = Math.max(0, Math.floor(topPosition / 30)); start.value = startIndex; const endIndex = startIndex + 300; data2.value = data.geneTexts.slice(startIndex, endIndex); let inner = document.getElementById('inner'); if (topPosition < 2700) { // 向上預計加載,這里判斷了三個高度,可以多判斷幾個,增加流暢度 inner.style.paddingTop = topPosition + 'px'; inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 - topPosition + 'px'; } else if (topPosition + data2.value.length * 30 >= data.geneTexts.length * 30) { // 這里 9000 是 內層div的高度 30 * 300 inner.style.paddingTop = topPosition - 900 + 'px'; //900 是div的高度 inner.style.paddingBottom = 0 + 'px'; } else { inner.style.paddingTop = topPosition - 2700 + 'px'; inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 + 2700 - topPosition + 'px'; } }; const data2 = ref([]); const init = () => { data2.value = data.geneTexts.slice(0, 300); let inner = document.getElementById('inner'); inner.style.paddingTop = 0 + 'px'; inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 - 900 + 'px'; }; </script> <style lang="less" scoped> // @import '../../../../assets/styles/views/medaka.less'; .button-box { margin-bottom: 25px; .flex-type(flex-end); :deep(.ant-btn) { margin-left: 10px; } } .enn { background: #282c34; outline: 1px solid red; padding: 30px 20px; height: 960px; } .download-box { width: 100%; // padding: 30px 20px; outline: 1px solid rgb(17, 0, 255); background-color: #fff; overflow: hidden; .line-box { .flex-type(flex-start); height: 30px; } &.txt { background: #282c34; color: #fff; height: 900px; overflow: auto; .el-row { display: flex; align-items: center; margin-bottom: 10px; margin: auto; font-size: 22px; } } } .form-box { .flex-type(center); } :deep(.ant-select-selector) { min-width: 120px; } @media screen and (min-width: 1842px) { .text-box-samll { letter-spacing: 1.5px; font-size: 15px; } .text-number-samll { min-width: 60px; font-size: 15px; } .text-number2-samll { margin-left: 20px; min-width: 60px; font-size: 15px; } .text-title-samll { font-size: 15px; } .text-box { font-size: 22px; // letter-spacing: 3px; } .text-number { min-width: 100px; font-size: 22px; } .text-number2 { margin-left: 20px; min-width: 100px; font-size: 22px; } .text-title { font-size: 22px; } } @media screen and (min-width: 1600px) and (max-width: 1841px) { .text-box-samll { font-size: 15px; } .text-number-samll { min-width: 40px; font-size: 15px; } .text-number2-samll { margin-left: 20px; min-width: 40px; font-size: 15px; } .text-title-samll { font-size: 15px; } .text-box { font-size: 20px; // letter-spacing: 1.2px; } .text-number { min-width: 60px; font-size: 15px; } .text-number2 { margin-left: 20px; min-width: 60px; font-size: 20px; } .text-title { font-size: 20px; } } @media screen and (min-width: 1443px) and (max-width: 1599px) { .text-box-samll { font-size: 13px; } .text-number-samll { min-width: 40px; font-size: 13px; } .text-number2-samll { margin-left: 20px; min-width: 40px; font-size: 13px; } .text-title-samll { font-size: 13px; } .text-box { font-size: 18px; // letter-spacing: 1.2px; } .text-number { min-width: 60px; font-size: 15px; } .text-number2 { margin-left: 20px; min-width: 60px; font-size: 18px; } .text-title { font-size: 18px; } } @media screen and (max-width: 1442px) { .text-box-samll { font-size: 11px; } .text-number-samll { min-width: 40px; font-size: 11px; } .text-number2-samll { margin-left: 20px; min-width: 40px; font-size: 11px; } .text-title-samll { font-size: 11px; } .text-box { font-size: 16px; // letter-spacing: 1.2px; } .text-number { min-width: 60px; font-size: 15px; } .text-number2 { margin-left: 20px; min-width: 60px; font-size: 16px; } .text-title { font-size: 16px; } } </style>
總結
到此這篇關于前端虛擬滾動列表(vue虛擬列表)的文章就介紹到這了,更多相關前端虛擬滾動列表內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Vue中使用Lodop插件實現(xiàn)打印功能的簡單方法
這篇文章主要給大家介紹了關于Vue中使用Lodop插件實現(xiàn)打印功能的簡單方法,文中通過示例代碼介紹的非常詳細,對大家學習或者使用Vue具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-12-12vue?Proxy數(shù)據(jù)代理進行校驗部分源碼實例解析
Proxy提供了強大的Javascript元編程,有許多功能,包括運算符重載,對象模擬,簡潔而靈活的API創(chuàng)建,對象變化事件,甚至Vue 3背后的內部響應系統(tǒng)提供動力,這篇文章主要給大家介紹了關于vue?Proxy數(shù)據(jù)代理進行校驗部分源碼解析的相關資料,需要的朋友可以參考下2022-01-01element?ui中el-form-item的屬性rules的用法示例小結
這篇文章主要介紹了element?ui中el-form-item的屬性rules的用法,本文通過實例代碼給大家介紹的非常詳細,感興趣的朋友一起看看吧2024-07-07Vue3?中的?readonly?特性及函數(shù)使用詳解
readonly是Vue3中提供的一個新特性,用于將一個響應式對象變成只讀對象,這篇文章主要介紹了Vue3?中的?readonly?特性詳解,需要的朋友可以參考下2023-04-04