Vue簡(jiǎn)易版無(wú)限加載組件實(shí)現(xiàn)原理與示例代碼
背景
遇到的兩個(gè)問題:scroll 事件不觸發(fā)、如何將 loading 狀態(tài)放在無(wú)限加載組件中進(jìn)行管理。
無(wú)限加載組件在展示列表頁(yè)數(shù)據(jù)時(shí)比較常見。特別是在 H5 列表頁(yè)中,數(shù)據(jù)比較多,需要做分頁(yè),無(wú)限加載組件就是一個(gè)非常好的選擇。
當(dāng)列表頁(yè)數(shù)據(jù)比較多時(shí),一次性從服務(wù)端拿到所有的數(shù)據(jù)會(huì)比較耗時(shí),長(zhǎng)時(shí)間不展示列表數(shù)據(jù),比較影響用戶體驗(yàn)。所以對(duì)于一般的長(zhǎng)列表數(shù)據(jù),都會(huì)做分頁(yè)。
首次請(qǐng)求時(shí),只請(qǐng)求第一頁(yè)數(shù)據(jù);當(dāng)用戶上拉即將到達(dá)列表底部時(shí),再請(qǐng)求下一頁(yè)數(shù)據(jù),將下一頁(yè)數(shù)據(jù)拼接在之前的列表后。
- mint-ui 無(wú)限加載組件體驗(yàn)地址:無(wú)限加載組件體驗(yàn)
實(shí)現(xiàn)功能
使用 vue3 composition API 實(shí)現(xiàn)如下功能:
- InfinitView 組件:將 InfinitView 組件包裹在列表(項(xiàng))外面即可實(shí)現(xiàn)無(wú)限加載。
- 節(jié)流加載:每次觸底加載時(shí),會(huì)自動(dòng)節(jié)流,同一頁(yè)數(shù)據(jù)只會(huì)請(qǐng)求一次(如果請(qǐng)求成功)。
注意:InfinitView 直接子元素高度需要比 InfinitView 組件高,才會(huì)觸發(fā)滾動(dòng)加載。InfinitView 組件的高度默認(rèn)為其父元素的 100%。
Props
// 觸底距離,當(dāng)距底部距離小于等于 distance 時(shí),會(huì)觸發(fā)加載函數(shù)
distance: {
type: Number,
default: 30,
},
// 加載函數(shù),觸底時(shí)執(zhí)行
onload: {
type: Function,
default: async () => {},
},
// 行內(nèi)樣式,在外部可以通過 classStyle 改變 InfinitView 組件的樣式
classStyle: {
type: Object,
default: () => ({}),
},
使用
直接將 InfiniteView 組件包裹在列表項(xiàng)外面即可:
<InfiniteView :onload="onload">
<div v-for="item in list">
{{ item }}
</div>
</InfiniteView>
使用 setTimeout 模擬列表數(shù)據(jù)的加載:
// nextPage 表示下一次請(qǐng)求哪一頁(yè)的數(shù)據(jù)
const nextPage = ref(1);
// list 表示數(shù)據(jù)列表
const list = ref(new Array(30).fill(0));
const onload = () => {
return new Promise((resolve) => {
setTimeout(() => {
list.value.push(...new Array(50).fill(0).map((_, i) => i + 1 + (nextPage.value - 1) * 50));
nextPage.value++;
resolve(nextPage.value);
}, 200);
});
}
使用時(shí)需要注意:InfinitView 組件的高度默認(rèn)為其父元素的 100%,如果其父元素高度不確定(例如:由子元素?fù)伍_),會(huì)導(dǎo)致 InfinitView 無(wú)法監(jiān)聽到滾動(dòng)事件,也就不會(huì)觸發(fā) onload 函數(shù)(后面會(huì)解釋原因)。
解決方案有兩種:
- 為 InfinitView 組件的父元素設(shè)置一個(gè)可計(jì)算的高度。
- 為 InfinitView 組件設(shè)置一個(gè)可計(jì)算的高度,可通過其 props 行內(nèi)樣式 classStyle 設(shè)置,或者在外部給 InfinitView 組件加上類名及其樣式。
注意:這里的可計(jì)算高度可以是:由 flex 彈性容器計(jì)算得來,但不能由子元素(InfinitView)撐開得來。
組件實(shí)現(xiàn)
組件的實(shí)現(xiàn)非常簡(jiǎn)單,InfinitView 組件實(shí)際上就是一個(gè) div,只不過在 InfinitView 內(nèi)部監(jiān)聽了該 div 的滾動(dòng)事件。在即將觸底的時(shí)候去調(diào)用從父組件中傳過來的 onload 函數(shù)。
其 template 實(shí)現(xiàn)如下:
<div class="infinite-view" :style="classStyle" @scroll="onScroll($event.target)" > <slot /> </div>
- 可以通過 classStyle 在外部設(shè)置 InfinitView 組件的樣式。
- 觸發(fā)滾動(dòng)事件的時(shí)候,會(huì)執(zhí)行 onScroll 函數(shù)。onScroll 函數(shù)中屏蔽了調(diào)用 onload 函數(shù)的細(xì)節(jié)(觸底加載、節(jié)流加載)。
- 使用 slot 將 InfinitView 的子組件當(dāng)成該 div 的子組件。
其 style 樣式如下:
.infinite-view {
height: 100%;
overflow-y: scroll;
}
InfinitView 組件的高度由其父元素決定,默認(rèn)為其父元素高度的 100%,這就限制了其父元素的高度不能由 InfinitView 撐開決定。
其 script 如下:
import { ref, defineProps } from 'vue';
const props = defineProps({
distance: {
type: Number,
default: 30,
},
onload: {
type: Function,
default: async () => {},
},
classStyle: {
type: Object,
default: () => ({}),
},
});
const isloading = ref(false);
const onScroll = async (element) => {
if (isloading.value) {
return;
}
if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) {
try {
isloading.value = true;
await props.onload();
isloading.value = false;
} catch (error) {
console.log(error);
isloading.value = false;
}
}
}
- 判斷觸底條件:
scrollHeight <= scrollTop + clientHeight + distance

- scrollHeight 代表整個(gè)滾動(dòng)區(qū)域的高度。
- scrollTop 是向上滾動(dòng)的距離,即從內(nèi)容區(qū)頂部到整個(gè)滾動(dòng)區(qū)域頂部的距離。
- clientHeight 是內(nèi)容區(qū)的高度。
- distance 代表觸底的距離,它是一個(gè)緩沖距離,即在內(nèi)容區(qū)底部距離整個(gè)滾動(dòng)區(qū)域底部距離小于等于 distance 時(shí),會(huì)觸發(fā) onload 函數(shù)。
當(dāng) scrollHeight === scrollTop + clientHeight 時(shí),剛好滑動(dòng)到底部。一般情況下,我們可以提前加載,設(shè)置一個(gè)緩沖距離 distance,當(dāng)即將滑動(dòng)到底部,距底部不足 distance 的距離時(shí),就可以觸發(fā)加載函數(shù)。
- isloading 用來控制加載的狀態(tài),并實(shí)現(xiàn)節(jié)流加載。
加載函數(shù) onload 一般是異步函數(shù),用來請(qǐng)求列表數(shù)據(jù)。在執(zhí)行 onload 函數(shù)之前,將 isloading 設(shè)為 true,表示正在加載中;當(dāng) onload 函數(shù)執(zhí)行完之后,將 isloading 設(shè)為 false,表示加載狀態(tài)結(jié)束。
我們知道,scroll 事件是會(huì)頻繁觸發(fā)的,只要列表在滾動(dòng),onScroll 函數(shù)就會(huì)一直執(zhí)行。
這就有可能導(dǎo)致:當(dāng)滑動(dòng)至距離底部不足 distance 距離時(shí),滿足觸底條件,列表還在持續(xù)滾動(dòng),此時(shí)就會(huì)持續(xù)執(zhí)行 onload 函數(shù)發(fā)送請(qǐng)求,即使上一次請(qǐng)求還沒回來,瀏覽器也會(huì)持續(xù)請(qǐng)求同一頁(yè)列表數(shù)據(jù)。
所以需要實(shí)現(xiàn)節(jié)流加載,控制 onload 函數(shù)的執(zhí)行頻率。如果上一次請(qǐng)求還沒回來,則不執(zhí)行 onload 函數(shù)。也就是在觸底條件之前,如果上一次請(qǐng)求還在加載中,直接 return 掉。
const onScroll = async (element) => {
// 如果上一次請(qǐng)求還沒回來,直接 return
if (isloading.value) {
return;
}
if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) {
try {
// 在請(qǐng)求之前將 isloading 置為 true
isloading.value = true;
await props.onload();
// 請(qǐng)求成功之后將 isloading 置為 false
isloading.value = false;
} catch (error) {
console.log(error);
// 請(qǐng)求失敗之后也將 isloading 置為 false
isloading.value = false;
}
}
}
實(shí)現(xiàn)的時(shí)候有兩個(gè)細(xì)節(jié)需要提一下:scroll 事件不觸發(fā)、如何將 loading 狀態(tài)放在無(wú)限加載組件中進(jìn)行管理。
scroll 事件
有時(shí)候經(jīng)常會(huì)遇到屏幕在滾動(dòng),但是一直沒有觸發(fā) scroll 事件。那是因?yàn)殡m然屏幕滾動(dòng)了,但是監(jiān)聽 scroll 事件的 div 并沒有滾動(dòng)。
當(dāng)然我們可以省事地為 window 設(shè)置監(jiān)聽 scroll 的事件,不管是哪個(gè)元素觸發(fā)的 scroll 事件,最終都會(huì)冒泡到 window 上面,設(shè)置的 scroll 回調(diào)函數(shù)也總是會(huì)執(zhí)行。
// 在 InfinitView 中,組件掛載之后,為 window 設(shè)置監(jiān)聽 scroll 的事件
onMounted(() => {
window.addEventListener('scroll', (e) => {
onScroll(e.target);
})
})
上面的代碼在 InfinitView 組件中,為 window 設(shè)置了監(jiān)聽 scroll 的事件。當(dāng)屏幕滾動(dòng)時(shí),就會(huì)執(zhí)行 onScroll 函數(shù)。這樣也是沒問題的,確實(shí)可以解決 scroll 事件不觸發(fā)的問題。
但是我們并沒有找到問題的根源,為什么在 InfinitView 組件中的 div 上面監(jiān)聽的 scroll 事件,卻不會(huì)觸發(fā)?
首先得知道什么情況下才會(huì)觸發(fā)滾動(dòng)事件:
- 父元素高度比其所有子元素高度之和小。
- 父元素的 overflow 屬性值為:
auto | scroll。
只有滿足了以上兩個(gè)條件,才會(huì)觸發(fā)父元素的 scroll 事件。很多時(shí)候,某個(gè) div 的 scroll 事件沒有觸發(fā),是因?yàn)槲覀儧]有設(shè)置該 div 的高度,它的高度由子元素?fù)伍_,和子元素高度之和相等。
這樣即使屏幕在滾動(dòng),觸發(fā)的也不是它的 scroll 事件,而是更上層 div 的 scroll 事件。例如:如果某個(gè) div 的高度由子元素?fù)伍_,并且其父元素高度確定,比它的高度小,則在滾動(dòng)的時(shí)候,不會(huì)觸發(fā)該 div 的 scroll 事件,會(huì)觸發(fā)它的父元素的 scroll 事件。
或者我們忘記設(shè)置監(jiān)聽 scroll 事件的元素的 overflow 屬性了,默認(rèn)情況下,overflow 的值為 visible。
$emit 發(fā)射事件和 props 回調(diào)函數(shù)的區(qū)別
我們知道在 Vue 中,子組件向父組件通信有兩種方式:通過 $emit 發(fā)射事件、通過調(diào)用父組件中傳過來的回調(diào)函數(shù)。
這兩種方式都可以由子組件向父組件通信,但是也有一些細(xì)微的區(qū)別:
- 通過調(diào)用父組件中傳過來的回調(diào)函數(shù)可以拿到函數(shù)的返回值,而通過
$emit發(fā)射事件不可以。 - 通過調(diào)用父組件中傳過來的回調(diào)函數(shù)可以知道函數(shù)什么時(shí)候執(zhí)行完,而通過
$emit發(fā)射事件不可以,它只能將回調(diào)函數(shù)通過$emit的參數(shù),傳給父組件,強(qiáng)迫父組件顯式調(diào)用,才能在函數(shù)執(zhí)行完之后做一些事情。
看起來,好像使用 props 回調(diào)函數(shù)比 $emit 發(fā)射事件要更好,那是不是 $emit 發(fā)射事件就沒有好處了呢?
也不是。從名字就可以看出,$emit 發(fā)射事件,是從子組件中發(fā)射一個(gè)事件給父組件,父組件在監(jiān)聽到子組件發(fā)射的事件之后,可以進(jìn)行一系列的操作。它只是給父組件發(fā)射一個(gè)事件,傳遞一個(gè)信號(hào)給父組件,父組件接收到這個(gè)信號(hào)之后,接下來要怎么做還是由父組件決定。但是使用 props 回調(diào)函數(shù)的方式則不同,它是將父組件中的一個(gè)函數(shù)通過 props 傳給子組件,子組件拿到這個(gè)回調(diào)函數(shù)之后,要怎么執(zhí)行,完全取決于子組件。所以子組件可以知道回調(diào)函數(shù)什么時(shí)候執(zhí)行完,也可以拿到回調(diào)函數(shù)的返回值。
了解了這兩種通信方式的區(qū)別,就解決了如何將 loading 狀態(tài)放在無(wú)限加載組件中進(jìn)行管理。
因?yàn)樾枰獙?loading 狀態(tài)放在無(wú)限加載組件(子組件)中進(jìn)行管理,所以無(wú)限加載組件(子組件)必須要知道請(qǐng)求什么時(shí)候回來,也就是 onload 異步函數(shù)什么時(shí)候執(zhí)行完。
這樣我們就可以用 props 回調(diào)函數(shù)的形式,將父組件中的異步請(qǐng)求函數(shù)傳給子組件,當(dāng)列表即將滾動(dòng)到底部時(shí),將 loading 狀態(tài)置為 true,然后發(fā)送請(qǐng)求,當(dāng)請(qǐng)求回來之后,異步函數(shù)執(zhí)行完,再將 loading 狀態(tài)置為 false。如果請(qǐng)求沒有回來,loading 狀態(tài)為 true,即使再次觸發(fā)了 scroll 事件,也直接返回,不再繼續(xù)發(fā)送請(qǐng)求。
const onScroll = async (element) => {
// 如果上一次請(qǐng)求還沒回來,直接 return
if (isloading.value) {
return;
}
if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) {
try {
// 在請(qǐng)求之前將 isloading 置為 true
isloading.value = true;
await props.onload();
// 請(qǐng)求成功之后將 isloading 置為 false
isloading.value = false;
} catch (error) {
console.log(error);
// 請(qǐng)求失敗之后也將 isloading 置為 false
isloading.value = false;
}
}
}總結(jié)
到此這篇關(guān)于Vue簡(jiǎn)易版無(wú)限加載組件實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Vue無(wú)限加載組件實(shí)現(xiàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Vue如何使用xlsx庫(kù)導(dǎo)出Excel文件
第三方庫(kù)xlsx提供了強(qiáng)大的功能來處理Excel文件,它可以簡(jiǎn)化導(dǎo)出Excel文件這個(gè)過程,本文將為大家詳細(xì)介紹一下它的具體使用,需要的小伙伴可以了解下2025-01-01
解決Vue使用swiper動(dòng)態(tài)加載數(shù)據(jù),動(dòng)態(tài)輪播數(shù)據(jù)顯示白屏的問題
今天小編就為大家分享一篇解決Vue使用swiper動(dòng)態(tài)加載數(shù)據(jù),動(dòng)態(tài)輪播數(shù)據(jù)顯示白屏的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-09-09
vue 實(shí)現(xiàn)在函數(shù)中觸發(fā)路由跳轉(zhuǎn)的示例
今天小編就為大家分享一篇vue 實(shí)現(xiàn)在函數(shù)中觸發(fā)路由跳轉(zhuǎn)的示例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-09-09
Vue使用Tinymce富文本自定義toolbar按鈕的實(shí)踐
本文主要介紹了Vue使用Tinymce富文本自定義toolbar按鈕,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
如何在vue-cli中使用css-loader實(shí)現(xiàn)css module
這篇文章主要介紹了如何在vue-cli中使用css-loader實(shí)現(xiàn)css module,幫助大家更好的理解和使用vue框架,感興趣的朋友可以了解下2021-01-01
vue-cli webpack 開發(fā)環(huán)境跨域詳解
本篇文章主要介紹了vue-cli webpack 開發(fā)環(huán)境跨域詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05
echarts實(shí)現(xiàn)獲取datazoom的起始值(包括x軸和y軸)
這篇文章主要介紹了echarts實(shí)現(xiàn)獲取datazoom的起始值(包括x軸和y軸),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-07-07

