Vue簡易版無限加載組件實現(xiàn)原理與示例代碼
背景
遇到的兩個問題:scroll 事件不觸發(fā)、如何將 loading 狀態(tài)放在無限加載組件中進行管理。
無限加載組件在展示列表頁數(shù)據(jù)時比較常見。特別是在 H5 列表頁中,數(shù)據(jù)比較多,需要做分頁,無限加載組件就是一個非常好的選擇。
當(dāng)列表頁數(shù)據(jù)比較多時,一次性從服務(wù)端拿到所有的數(shù)據(jù)會比較耗時,長時間不展示列表數(shù)據(jù),比較影響用戶體驗。所以對于一般的長列表數(shù)據(jù),都會做分頁。
首次請求時,只請求第一頁數(shù)據(jù);當(dāng)用戶上拉即將到達列表底部時,再請求下一頁數(shù)據(jù),將下一頁數(shù)據(jù)拼接在之前的列表后。
- mint-ui 無限加載組件體驗地址:無限加載組件體驗
實現(xiàn)功能
使用 vue3 composition API 實現(xiàn)如下功能:
- InfinitView 組件:將 InfinitView 組件包裹在列表(項)外面即可實現(xiàn)無限加載。
- 節(jié)流加載:每次觸底加載時,會自動節(jié)流,同一頁數(shù)據(jù)只會請求一次(如果請求成功)。
注意:InfinitView 直接子元素高度需要比 InfinitView 組件高,才會觸發(fā)滾動加載。InfinitView 組件的高度默認為其父元素的 100%。
Props
// 觸底距離,當(dāng)距底部距離小于等于 distance 時,會觸發(fā)加載函數(shù) distance: { type: Number, default: 30, }, // 加載函數(shù),觸底時執(zhí)行 onload: { type: Function, default: async () => {}, }, // 行內(nèi)樣式,在外部可以通過 classStyle 改變 InfinitView 組件的樣式 classStyle: { type: Object, default: () => ({}), },
使用
直接將 InfiniteView 組件包裹在列表項外面即可:
<InfiniteView :onload="onload"> <div v-for="item in list"> {{ item }} </div> </InfiniteView>
使用 setTimeout 模擬列表數(shù)據(jù)的加載:
// nextPage 表示下一次請求哪一頁的數(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); }); }
使用時需要注意:InfinitView 組件的高度默認為其父元素的 100%,如果其父元素高度不確定(例如:由子元素撐開),會導(dǎo)致 InfinitView 無法監(jiān)聽到滾動事件,也就不會觸發(fā) onload 函數(shù)(后面會解釋原因)。
解決方案有兩種:
- 為 InfinitView 組件的父元素設(shè)置一個可計算的高度。
- 為 InfinitView 組件設(shè)置一個可計算的高度,可通過其 props 行內(nèi)樣式 classStyle 設(shè)置,或者在外部給 InfinitView 組件加上類名及其樣式。
注意:這里的可計算高度可以是:由 flex 彈性容器計算得來,但不能由子元素(InfinitView)撐開得來。
組件實現(xiàn)
組件的實現(xiàn)非常簡單,InfinitView 組件實際上就是一個 div,只不過在 InfinitView 內(nèi)部監(jiān)聽了該 div 的滾動事件。在即將觸底的時候去調(diào)用從父組件中傳過來的 onload 函數(shù)。
其 template 實現(xiàn)如下:
<div class="infinite-view" :style="classStyle" @scroll="onScroll($event.target)" > <slot /> </div>
- 可以通過 classStyle 在外部設(shè)置 InfinitView 組件的樣式。
- 觸發(fā)滾動事件的時候,會執(zhí)行 onScroll 函數(shù)。onScroll 函數(shù)中屏蔽了調(diào)用 onload 函數(shù)的細節(jié)(觸底加載、節(jié)流加載)。
- 使用 slot 將 InfinitView 的子組件當(dāng)成該 div 的子組件。
其 style 樣式如下:
.infinite-view { height: 100%; overflow-y: scroll; }
InfinitView 組件的高度由其父元素決定,默認為其父元素高度的 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 代表整個滾動區(qū)域的高度。
- scrollTop 是向上滾動的距離,即從內(nèi)容區(qū)頂部到整個滾動區(qū)域頂部的距離。
- clientHeight 是內(nèi)容區(qū)的高度。
- distance 代表觸底的距離,它是一個緩沖距離,即在內(nèi)容區(qū)底部距離整個滾動區(qū)域底部距離小于等于 distance 時,會觸發(fā) onload 函數(shù)。
當(dāng) scrollHeight === scrollTop + clientHeight
時,剛好滑動到底部。一般情況下,我們可以提前加載,設(shè)置一個緩沖距離 distance,當(dāng)即將滑動到底部,距底部不足 distance 的距離時,就可以觸發(fā)加載函數(shù)。
- isloading 用來控制加載的狀態(tài),并實現(xiàn)節(jié)流加載。
加載函數(shù) onload 一般是異步函數(shù),用來請求列表數(shù)據(jù)。在執(zhí)行 onload 函數(shù)之前,將 isloading 設(shè)為 true,表示正在加載中;當(dāng) onload 函數(shù)執(zhí)行完之后,將 isloading 設(shè)為 false,表示加載狀態(tài)結(jié)束。
我們知道,scroll 事件是會頻繁觸發(fā)的,只要列表在滾動,onScroll 函數(shù)就會一直執(zhí)行。
這就有可能導(dǎo)致:當(dāng)滑動至距離底部不足 distance 距離時,滿足觸底條件,列表還在持續(xù)滾動,此時就會持續(xù)執(zhí)行 onload 函數(shù)發(fā)送請求,即使上一次請求還沒回來,瀏覽器也會持續(xù)請求同一頁列表數(shù)據(jù)。
所以需要實現(xiàn)節(jié)流加載,控制 onload 函數(shù)的執(zhí)行頻率。如果上一次請求還沒回來,則不執(zhí)行 onload 函數(shù)。也就是在觸底條件之前,如果上一次請求還在加載中,直接 return 掉。
const onScroll = async (element) => { // 如果上一次請求還沒回來,直接 return if (isloading.value) { return; } if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) { try { // 在請求之前將 isloading 置為 true isloading.value = true; await props.onload(); // 請求成功之后將 isloading 置為 false isloading.value = false; } catch (error) { console.log(error); // 請求失敗之后也將 isloading 置為 false isloading.value = false; } } }
實現(xiàn)的時候有兩個細節(jié)需要提一下:scroll 事件不觸發(fā)、如何將 loading 狀態(tài)放在無限加載組件中進行管理。
scroll 事件
有時候經(jīng)常會遇到屏幕在滾動,但是一直沒有觸發(fā) scroll 事件。那是因為雖然屏幕滾動了,但是監(jiān)聽 scroll 事件的 div 并沒有滾動。
當(dāng)然我們可以省事地為 window 設(shè)置監(jiān)聽 scroll 的事件,不管是哪個元素觸發(fā)的 scroll 事件,最終都會冒泡到 window 上面,設(shè)置的 scroll 回調(diào)函數(shù)也總是會執(zhí)行。
// 在 InfinitView 中,組件掛載之后,為 window 設(shè)置監(jiān)聽 scroll 的事件 onMounted(() => { window.addEventListener('scroll', (e) => { onScroll(e.target); }) })
上面的代碼在 InfinitView 組件中,為 window 設(shè)置了監(jiān)聽 scroll 的事件。當(dāng)屏幕滾動時,就會執(zhí)行 onScroll 函數(shù)。這樣也是沒問題的,確實可以解決 scroll 事件不觸發(fā)的問題。
但是我們并沒有找到問題的根源,為什么在 InfinitView 組件中的 div 上面監(jiān)聽的 scroll 事件,卻不會觸發(fā)?
首先得知道什么情況下才會觸發(fā)滾動事件
:
- 父元素高度比其所有子元素高度之和小。
- 父元素的 overflow 屬性值為:
auto | scroll
。
只有滿足了以上兩個條件,才會觸發(fā)父元素的 scroll 事件。很多時候,某個 div 的 scroll 事件沒有觸發(fā),是因為我們沒有設(shè)置該 div 的高度,它的高度由子元素撐開,和子元素高度之和相等。
這樣即使屏幕在滾動,觸發(fā)的也不是它的 scroll 事件,而是更上層 div 的 scroll 事件。例如:如果某個 div 的高度由子元素撐開,并且其父元素高度確定,比它的高度小,則在滾動的時候,不會觸發(fā)該 div 的 scroll 事件,會觸發(fā)它的父元素的 scroll 事件。
或者我們忘記設(shè)置監(jiān)聽 scroll 事件的元素的 overflow 屬性了,默認情況下,overflow 的值為 visible。
$emit 發(fā)射事件和 props 回調(diào)函數(shù)的區(qū)別
我們知道在 Vue 中,子組件向父組件通信有兩種方式:通過 $emit
發(fā)射事件、通過調(diào)用父組件中傳過來的回調(diào)函數(shù)。
這兩種方式都可以由子組件向父組件通信,但是也有一些細微的區(qū)別:
- 通過調(diào)用父組件中傳過來的回調(diào)函數(shù)可以拿到函數(shù)的返回值,而通過
$emit
發(fā)射事件不可以。 - 通過調(diào)用父組件中傳過來的回調(diào)函數(shù)可以知道函數(shù)什么時候執(zhí)行完,而通過
$emit
發(fā)射事件不可以,它只能將回調(diào)函數(shù)通過$emit
的參數(shù),傳給父組件,強迫父組件顯式調(diào)用,才能在函數(shù)執(zhí)行完之后做一些事情。
看起來,好像使用 props 回調(diào)函數(shù)比 $emit
發(fā)射事件要更好,那是不是 $emit
發(fā)射事件就沒有好處了呢?
也不是。從名字就可以看出,$emit
發(fā)射事件,是從子組件中發(fā)射一個事件給父組件,父組件在監(jiān)聽到子組件發(fā)射的事件之后,可以進行一系列的操作。它只是給父組件發(fā)射一個事件,傳遞一個信號給父組件,父組件接收到這個信號之后,接下來要怎么做還是由父組件決定。但是使用 props 回調(diào)函數(shù)的方式則不同,它是將父組件中的一個函數(shù)通過 props 傳給子組件,子組件拿到這個回調(diào)函數(shù)之后,要怎么執(zhí)行,完全取決于子組件。所以子組件可以知道回調(diào)函數(shù)什么時候執(zhí)行完,也可以拿到回調(diào)函數(shù)的返回值。
了解了這兩種通信方式的區(qū)別,就解決了如何將 loading 狀態(tài)放在無限加載組件中進行管理。
因為需要將 loading 狀態(tài)放在無限加載組件(子組件)中進行管理,所以無限加載組件(子組件)必須要知道請求什么時候回來,也就是 onload 異步函數(shù)什么時候執(zhí)行完。
這樣我們就可以用 props 回調(diào)函數(shù)的形式,將父組件中的異步請求函數(shù)傳給子組件,當(dāng)列表即將滾動到底部時,將 loading 狀態(tài)置為 true,然后發(fā)送請求,當(dāng)請求回來之后,異步函數(shù)執(zhí)行完,再將 loading 狀態(tài)置為 false。如果請求沒有回來,loading 狀態(tài)為 true,即使再次觸發(fā)了 scroll 事件,也直接返回,不再繼續(xù)發(fā)送請求。
const onScroll = async (element) => { // 如果上一次請求還沒回來,直接 return if (isloading.value) { return; } if (element.scrollHeight <= element.scrollTop + element.offsetHeight + props.distance) { try { // 在請求之前將 isloading 置為 true isloading.value = true; await props.onload(); // 請求成功之后將 isloading 置為 false isloading.value = false; } catch (error) { console.log(error); // 請求失敗之后也將 isloading 置為 false isloading.value = false; } } }
總結(jié)
到此這篇關(guān)于Vue簡易版無限加載組件實現(xiàn)原理的文章就介紹到這了,更多相關(guān)Vue無限加載組件實現(xiàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue-loader中引入模板預(yù)處理器的實現(xiàn)
這篇文章主要介紹了vue-loader中引入模板預(yù)處理器的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09Vue中 Runtime + Compiler 和 Runtime-o
這篇文章主要介紹了Vue中 Runtime + Compiler 和 Runtime-only 兩種模式含義和區(qū)別,結(jié)合實例形式詳細分析了Vue中 Runtime + Compiler 和 Runtime-only 兩種模式基本功能、原理、區(qū)別與相關(guān)注意事項,需要的朋友可以參考下2023-06-06在線使用iconfont字體圖標(biāo)的簡單實現(xiàn)
這篇文章主要介紹了在線使用iconfont字體圖標(biāo)的簡單實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09使用Vue3和Echarts?5繪制帶有立體感流線中國地圖(推薦收藏!)
最近接到一個需求是做一個中國地圖,下面這篇文章主要給大家介紹了關(guān)于如何使用Vue3和Echarts?5繪制帶有立體感流線中國地圖的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下2022-04-04基于vue實現(xiàn)多功能樹形結(jié)構(gòu)組件的示例代碼
一個優(yōu)雅展示樹形結(jié)構(gòu)數(shù)據(jù)的 Vue 組件,遞歸渲染每個節(jié)點及其子節(jié)點,支持自定義顏色、文本和布局,通過獨特的樣式巧妙處理不同層級,為用戶打造豐富的視覺盛宴,文中通過代碼給大家介紹的非常詳細,感興趣的同學(xué)可以自己動手嘗試一下2024-02-02Vue中watch與watchEffect的區(qū)別詳細解讀
這篇文章主要介紹了Vue中watch與watchEffect的區(qū)別詳細解讀,watch函數(shù)與watchEffect函數(shù)都是監(jiān)聽器,在寫法和用法上有一定區(qū)別,是同一功能的兩種不同形態(tài),底層都是一樣的,需要的朋友可以參考下2023-11-11