虛擬列表,我真的會了!!!
點(diǎn)擊上方?
程序員成長指北
,關(guān)注公眾號
回復(fù) 1 ,加入高級Node交流群
原文鏈接:?https://juejin.cn/post/7085941958228574215
作者:Running53
虛擬列表的使用場景
如果我想要在網(wǎng)頁中放大量的列表項,純渲染的話,對于瀏覽器性能將會是個極大的挑戰(zhàn),會造成滾動卡頓,整體體驗非常不好,主要有以下問題:
- 頁面等待時間極長,用戶體驗差
- CPU計算能力不夠,滑動會卡頓
- GPU渲染能力不夠,頁面會跳屏
- RAM內(nèi)存容量不夠,瀏覽器崩潰
1. 傳統(tǒng)做法
對于長列表渲染,傳統(tǒng)的方法是使用懶加載的方式,下拉到底部獲取新的內(nèi)容加載進(jìn)來,其實就相當(dāng)于是在垂直方向上的分頁疊加功能,**但隨著加載數(shù)據(jù)越來越多,瀏覽器的回流和重繪的開銷將會越來越大**,整個滑動也會造成卡頓,這個時候我們就可以考慮使用虛擬列表來解決問題
2. 虛擬列表
其核心思想就是在處理用戶滾動時,只改變列表在可視區(qū)域的渲染部分,具體步驟為:
先計算可見區(qū)域起始數(shù)據(jù)的索引值startIndex和當(dāng)前可見區(qū)域結(jié)束數(shù)據(jù)的索引值endIndex,假如元素的高度是固定的,那么startIndex的算法很簡單,即startIndex = Math.floor(scrollTop/itemHeight),endIndex = startIndex + (clientHeight/itemHeight) - 1,再根據(jù)startIndex?和endIndex取相應(yīng)范圍的數(shù)據(jù),渲染到可視區(qū)域,然后再計算startOffset(上滾動空白區(qū)域)和endOffset(下滾動空白區(qū)域),這兩個偏移量的作用就是來撐開容器元素的內(nèi)容,從而起到緩沖的作用,使得滾動條保持平滑滾動,并使?jié)L動條處于一個正確的位置
上述的操作可以總結(jié)成五步:
- 不把長列表數(shù)據(jù)一次性全部直接渲染在頁面上
- 截取長列表一部分?jǐn)?shù)據(jù)用來填充可視區(qū)域
-
長列表數(shù)據(jù)不可視部分使用空白占位填充(下圖中的
startOffset和endOffset區(qū)域) - 監(jiān)聽滾動事件根據(jù)滾動位置動態(tài)改變可視列表
- 監(jiān)聽滾動事件根據(jù)滾動位置動態(tài)改變空白填充

定高虛擬列表實現(xiàn)步驟
掘金使用的是傳統(tǒng)懶加載的方式加載的哈,用的并不是虛擬列表,這里只是想表達(dá)一下什么是定高的列表!

實現(xiàn)的效果應(yīng)該是:不論怎么滾動,我們改變的只是滾動條的高度和可視區(qū)的元素內(nèi)容,并沒有增加任何多余的元素,下面來看看要怎么實現(xiàn)吧!
//?虛擬列表DOM結(jié)構(gòu)
<div?className='container'>
??//?監(jiān)聽滾動事件的盒子,該高度繼承了父元素的高度
??<div?className='scroll-box'?ref={containerRef}?onScroll={boxScroll}>
????//?該盒子的高度一定會超過父元素,要不實現(xiàn)不了滾動的效果,而且還要動態(tài)的改變它的padding值用于控制滾動條的狀態(tài)
????<div?style={topBlankFill.current}>
??????{
??????showList.map(item?=>?<div?className='item'?key={item.commentId?||?(Math.random()?+?item.comments)}>{item.content}</div>)
??????}
????</div>
??</div>
</div>
計算容器最大容積數(shù)量
簡單來說,就是我們必須要知道在可視區(qū)域內(nèi)最多能夠容納多少個列表項,這是我們在截取內(nèi)容數(shù)據(jù)渲染到頁面之前關(guān)鍵的步驟之一
?//?滾動容器高度改變后執(zhí)行的函數(shù)
const?changeHeight?=?useCallback(throttle(()?=>?{
??//?容器高度,通過操作dom元素獲取高度是因為它不一定是個定值
??curContainerHeight.current?=?containerRef.current.offsetHeight
??//?列表最大數(shù)量,考慮到列表中頂部和底部可能都會出現(xiàn)沒有展現(xiàn)完的item
??curViewNum.current?=?Math.ceil(curContainerHeight.current?/?itemHeight)?+?1
},?500),?[])
useEffect(()?=>?{
??//?組件第一次掛載需要初始化容器的高度以及最大容納值
??changeHeight()
??//?因為我們的可視窗口和瀏覽器大小有關(guān)系,所以我們需要監(jiān)聽瀏覽器大小的變化
??//?當(dāng)瀏覽器大小改變之后需要重新執(zhí)行changeHeight函數(shù)計算當(dāng)前可視窗口對應(yīng)的最大容納量是多少
??window.addEventListener('resize',?changeHeight)
??return?()?=>?{
????window.removeEventListener('resize',?changeHeight)
??}
},?[changeHeight])
監(jiān)聽滾動事件動態(tài)截取數(shù)據(jù)&&設(shè)置上下滾動緩沖消除快速滾動白屏
這是虛擬列表的核心之處,不將所有我們請求到的元素渲染出來,而是只渲染我們能夠看到的元素,大大減少了容器內(nèi)的dom節(jié)點(diǎn)數(shù)量。
不過有個隱藏的問題我們需要考慮到,當(dāng)用戶滑動過快的時候,很多用戶的設(shè)備性能并不是很好,很容易出現(xiàn)屏幕已經(jīng)滾動過去了,但是列表項還沒有及時加載出來的情況,這個時候用戶就會看到短暫的白屏,對用戶的體驗非常不好。所以我們需要設(shè)置一段緩沖區(qū)域,讓用戶過快的滾動之后還能看到我們提前渲染好的數(shù)據(jù),等到緩沖數(shù)據(jù)滾動完了,我們新的數(shù)據(jù)也渲染到頁面中去了!
const?scrollHandle?=?()?=>?{
??//?注意這個對應(yīng)的是可視區(qū)第一個元素的索引值,而不是第多少個元素
??let?startIndex?=?Math.floor(containerRef.current.scrollTop?/?itemHeight)?//?itemHeight是列表每一項的高度
??//?優(yōu)化:如果是用戶滾動觸發(fā)的,而且兩次startIndex的值都一樣,那么就沒有必要執(zhí)行下面的邏輯
??if?(!isNeedLoad?&&?lastStartIndex.current?===?startIndex)?return
??isNeedLoad.current?=?false
??lastStartIndex.current?=?startIndex
??const?containerMaxSize?=?curViewNum.current
??/**
???*?解決滑動過快出現(xiàn)的白屏問題:注意endIndex要在startIndex人為改變之前就計算好
???*?因為我們實際上需要三板的數(shù)據(jù)用于兼容低性能的設(shè)備,用做上下滾動的緩沖區(qū)域,避免滑動的時候出現(xiàn)白屏
???*?現(xiàn)在的startIndex是可視區(qū)的第一個元素索引,再加上2倍可視區(qū)元素量,剛好在下方就會多出一板來當(dāng)做緩沖區(qū)
???*/
??//?此處的endIndex是為了在可視區(qū)域的下方多出一板數(shù)據(jù)
??let?endIndex?=?startIndex?+?2?*?containerMaxSize?-?1
??//?接近滾動到屏幕底部的時候,就可以請求發(fā)送數(shù)據(jù)了,這個時候觸底的并不是可視區(qū)的最后一個元素,而是多出那一版的最后一個元素觸底了
??const?currLen?=?dataListRef.current.length
??if?(endIndex?>?currLen?-?1)?{
????//?更新請求參數(shù),發(fā)送請求獲取新的數(shù)據(jù)(但是要保證當(dāng)前不在請求過程中,否則就會重復(fù)請求相同的數(shù)據(jù))
????!isRequestRef.current?&&?setOptions(state?=>?({?offset:?state.offset?+?1?}))
????//?如果已經(jīng)滾動到了底部,那么就設(shè)置endIndex為最后一個元素索引即可
????endIndex?=?currLen?-?1
??}
??//?此處的endIndex是為了在可視區(qū)域的上方多出一板數(shù)據(jù)
??//?這里人為的調(diào)整startIndex的值,目的就是為了能夠在可視區(qū)域上方多出一板來當(dāng)做緩沖區(qū)
??if?(startIndex?<=?containerMaxSize)?{?//?containerMaxSize是我們之前計算出來的容器容納量
????startIndex?=?0
??}?else?{
????startIndex?=?startIndex?-?containerMaxSize
??}
??//?使用slice方法截取數(shù)據(jù),但是要記住第二個參數(shù)對應(yīng)的索引元素不會被刪除,最多只能刪除到它的前一個,所以我們這里的endIndex需要加一
??setShowList(dataListRef.current.slice(startIndex,?endIndex?+?1))
}
動態(tài)設(shè)置上下空白占位
這是虛擬列表的靈魂所在,本質(zhì)上我們數(shù)據(jù)量是很少的,一般來說只有幾條到十幾條數(shù)據(jù),如果不對列表做一些附加的操作,連生成滾動條都有點(diǎn)困難,更別說讓用戶自由操控滾動條滾動了。
我們必須要用某種方法將內(nèi)容區(qū)域撐起來,這樣才會出現(xiàn)比較合適的滾動條。我這里采取的方法就是設(shè)置paddingTop和paddingBottom的值來動態(tài)的撐開內(nèi)容區(qū)域。
為什么要動態(tài)的改變呢?舉個例子,我們向下滑動的時候會更換頁面中要展示的數(shù)據(jù)列表,如果不改變原先的空白填充區(qū)域,那么隨著滾動條的滾動,原先展示在可視區(qū)的第一條數(shù)據(jù)就會向上移動,雖然我們更新的數(shù)據(jù)是正確的,但并沒有將它們展示在合適的位置。完美的方案是是不僅要展示正確的數(shù)據(jù),而且還要改變空白填充區(qū)域高度,使得數(shù)據(jù)能夠正確的展示在瀏覽器視口當(dāng)中。
//?以下代碼要放在更新列表數(shù)據(jù)之前,也是在滾動事件boxScroll當(dāng)中
//?改變空白填充區(qū)域的樣式,否則就會出現(xiàn)可視區(qū)域的元素與滾動條不匹配的情況,實現(xiàn)不了平滑滾動的效果
topBlankFill.current?=?{
??//?起始索引就是緩沖區(qū)第一個元素的索引,索引為多少就代表前面有多少個元素
??paddingTop:?`${startIndex?*?itemHeight}px`,
??//?endIndex是緩沖區(qū)的最后一個元素,可能不在可視區(qū)內(nèi);用dataListRef數(shù)組最后一個元素的索引與endIndex相減就可以得到還沒有渲染元素的數(shù)目
??paddingBottom:?`${(dataListRef.current.length?-?1?-?endIndex)?*?itemHeight}px`
}
下拉置地自動請求和加載數(shù)據(jù)
在真實的開發(fā)場景中,我們不會一次性請求1w、10w條數(shù)據(jù)過來,這樣請求時間那么長,用戶早就把頁面關(guān)掉了,還優(yōu)化個屁啊哈哈!
所以真實開發(fā)中,我們還是要結(jié)合原來的懶加載方式,等到下拉觸底的時候去加載新的數(shù)據(jù)進(jìn)來,放置到緩存數(shù)據(jù)當(dāng)中,然后我們再根據(jù)滾動事件決定具體渲染哪一部分的數(shù)據(jù)到頁面上去。
//?組件剛掛載以及下拉觸底的時候請求更多數(shù)據(jù)
useEffect(()?=>?{
??(async?()?=>?{
????try?{
??????//?表明當(dāng)前正處于請求過程中
??????isRequestRef.current?=?true
??????const?{?offset?}?=?options
??????let?limit?=?20
??????if?(offset?===?1)?limit?=?40
??????const?{?data:?{?comments,?more?}?}?=?await?axios({
????????url:?`http://localhost:3000/comment/music?id=${186015?-?offset}&limit=${limit}&offset=1`
??????})
??????isNeedLoad.current?=?more
??????//?將新請求到的數(shù)據(jù)添加到存儲列表數(shù)據(jù)的變量當(dāng)中
??????dataListRef.current?=?[...dataListRef.current,?...comments]
??????//?必選要在boxScroll之前將isRequestRef設(shè)為false,因為boxScroll函數(shù)內(nèi)部會用到這個變量
??????isRequestRef.current?=?false
??????//?請求完最新數(shù)據(jù)的時候需要重新觸發(fā)一下boxScroll函數(shù),因為容器內(nèi)的數(shù)據(jù)、空白填充區(qū)域可能需要變化
??????boxScroll()
????}?catch?(err)?{
??????isRequestRef.current?=?false
??????console.log(err);
????}
??})()
??//?在boxScroll函數(shù)里面,一旦發(fā)生了觸底操作就會去改變optiosn的值
},?[options])
滾動事件請求動畫幀進(jìn)行節(jié)流優(yōu)化
虛擬列表很依賴于滾動事件,考慮到用戶可能會滑動很快,我們在用節(jié)流優(yōu)化的時候事件必須要設(shè)置的夠短,否則還是會出現(xiàn)白屏現(xiàn)象。
這里我沒有用傳統(tǒng)的節(jié)流函數(shù),而是用到了請求動畫幀幫助我們進(jìn)行節(jié)流,這里我就不做具體介紹了,想了解的可以看我另一篇文章juejin.cn/post/708236…[1]juejin.cn/post/684490…[2]
//?利用請求動畫幀做了一個節(jié)流優(yōu)化
let?then?=?useRef(0)
const?boxScroll?=?()?=>?{
??const?now?=?Date.now()
??/**
???*?這里的等待時間不宜設(shè)置過長,不然會出現(xiàn)滑動到空白占位區(qū)域的情況
???*?因為間隔時間過長的話,太久沒有觸發(fā)滾動更新事件,下滑就會到padding-bottom的空白區(qū)域
???*?電腦屏幕的刷新頻率一般是60HZ,渲染的間隔時間為16.6ms,我們的時間間隔最好小于兩次渲染間隔16.6*2=33.2ms,一般情況下30ms左右,
???*/
??if?(now?-?then.current?>?30)?{
????then.current?=?now
????//?重復(fù)調(diào)用scrollHandle函數(shù),讓瀏覽器在下一次重繪之前執(zhí)行函數(shù),可以確保不會出現(xiàn)丟幀現(xiàn)象
????window.requestAnimationFrame(scrollHandle)
??}
}
當(dāng)然,填充空白區(qū)域、模擬滾動條還有其它的辦法,比如根據(jù)總數(shù)據(jù)量讓一個盒子撐開父盒子用于生成滾動條,根據(jù)startIndex計算出可視區(qū)域距離頂部的距離并調(diào)節(jié)內(nèi)容區(qū)域元素的transform屬性,即startOffset = scrollTop - (scrollTop % this.itemSize),讓內(nèi)容區(qū)域一直暴露在可視區(qū)域內(nèi)
目前為止,我們已經(jīng)實現(xiàn)了固定高度的列表項用虛擬列表來展示的功能!接下里我們將會介紹關(guān)于不定高(其高度由內(nèi)容進(jìn)行撐開)的列表項如何用虛擬列表進(jìn)行優(yōu)化
不定高虛擬列表實現(xiàn)步驟
微博是一個很典型的不定高虛擬列表,大家感興趣的話可以去看一下哦!

在之前的實現(xiàn)中,列表項的高度是固定的,因為高度固定,所以可以很輕易的就能獲取列表項的整體高度以及滾動時的顯示數(shù)據(jù)與對應(yīng)的偏移量。而實際應(yīng)用的時候,當(dāng)列表中包含文本、圖片之類的可變內(nèi)容,會導(dǎo)致列表項的高度并不相同。
我們在列表渲染之前,確實沒有辦法知道每一項的高度,但是又必須要渲染出來,那怎么辦呢?
這里有一個解決方法,就是先給沒有渲染出來的列表項設(shè)置一個預(yù)估高度,等到這些數(shù)據(jù)渲染成真實dom元素了之后,再獲取到他們的真實高度去更新原來設(shè)置的預(yù)估高度,下面我們來看看跟定高列表有什么不同,具體要怎么實現(xiàn)吧!
請求到新數(shù)據(jù)對數(shù)據(jù)進(jìn)行初始化(設(shè)置預(yù)估高度)
預(yù)估高度的設(shè)置其實是有技巧的,列表項預(yù)估高度設(shè)置的越大,展現(xiàn)出來的數(shù)據(jù)就會越少,所以當(dāng)預(yù)估高度比實際高度大很多的時候,很容易出現(xiàn)可視區(qū)域數(shù)據(jù)量太少而引起的可視區(qū)域出現(xiàn)部分空白。為了避免這種情況,我們的預(yù)估高度應(yīng)該設(shè)置為列表項產(chǎn)生的最小值,這樣盡管可能會多渲染出幾條數(shù)據(jù),但能保證首次呈現(xiàn)給用戶的畫面中沒有空白
//?請求更多的數(shù)據(jù)
useEffect(()?=>?{
??(async?()?=>?{
????//?只有當(dāng)前不在請求狀態(tài)的時候才可以發(fā)送新的請求
????if?(!isRequestRef.current)?{
??????console.log('發(fā)送請求了');
??????try?{
????????isRequestRef.current?=?true
????????const?{?offset?}?=?options
????????let?limit?=?20
????????if?(offset?===?1)?limit?=?40
????????const?{?data:?{?comments,?more?}?}?=?await?axios({
??????????url:?`http://localhost:3000/comment/music?id=${186015?-?offset}&limit=${limit}&offset=1`
????????})
????????isNeedLoad.current?=?more
????????//?獲取緩存中最后一個數(shù)據(jù)的索引值,如果沒有,則返回-1
????????const?lastIndex?=?dataListRef.current.length???dataListRef.current[dataListRef.current.length?-?1].index?:?-1
????????//?先將請求到的數(shù)據(jù)添加到緩存數(shù)組中去
????????dataListRef.current?=?[...dataListRef.current,?...comments]
????????const?dataList?=?dataListRef.current
????????//?將剛剛請求到的新數(shù)據(jù)做一下處理,為他們添加上對應(yīng)的索引值、預(yù)估高度、以及元素首尾距離容器頂部的距離
????????for?(let?i?=?lastIndex?+?1,?len?=?dataListRef.current.length;?i?<?len;?i++)?{
??????????dataList[i].index?=?i
??????????//?預(yù)估高度是列表項對應(yīng)的最小高度
??????????dataList[i].height?=?63
??????????//?每一個列表項頭部距離容器頂部的距離等于上一個元素尾部距離容器頂部的距離
??????????dataList[i].top?=?dataList[i?-?1]?.bottom?||?0
??????????//?每一個列表項尾部距離容器頂部的距離等于上一個元素頭部距離容器頂部的距離加上自身列表項的高度
??????????dataList[i].bottom?=?dataList[i].top?+?dataList[i].height
????????}
????????isRequestRef.current?=?false
????????boxScroll()
??????}?catch?(err)?{
????????console.log(err);
??????}?finally?{
????????isRequestRef.current?=?false
??????}
????}
??})()
??//?eslint-disable-next-line
},?[options])
每次列表更新之后將列表項真實高度更新緩存中的預(yù)估高度
在React函數(shù)式組件中,useEffect只要不傳第二個參數(shù),就可以實現(xiàn)類組件componentDidUpdate生命周期函數(shù)的作用,只要我們重新渲染一次列表組件,就會重新計算一下當(dāng)前列表每一項中的真實高度并更新到緩存中去,當(dāng)下次我們再用到緩存中的這些數(shù)據(jù)時,使用的就是真實高度了
//?每次組件重新渲染即用戶滾動更改了數(shù)據(jù)之后需要將列表中我們還不知道的列表項高度更新到我們的緩存數(shù)據(jù)中去,以便下一次更新的時候能夠正常渲染
useEffect(()?=>?{?
??const?doms?=?containerRef.current.children[0].children
??const?len?=?doms.length
??//?因為一開始我們沒有請求數(shù)據(jù),所以即使組件渲染完了,但是沒有列表項,此時不需要執(zhí)行后續(xù)操作
??if?(len)?{
????//?遍歷所有的列表結(jié)點(diǎn),根據(jù)結(jié)點(diǎn)的真實高度去更改緩存中的高度
????for?(let?i?=?0;?i?<?len;?i++)?{
??????const?realHeight?=?doms[i].offsetHeight
??????const?originHeight?=?showList[i].height
??????const?dValue?=?realHeight?-?originHeight
??????//?如果列表項的真實高度就是緩存中的高度,則不需要進(jìn)行更新
??????if?(dValue)?{
????????const?index?=?showList[i].index
????????const?allData?=?dataListRef.current
????????/**
???????????*?如果列表項的真實高度不是緩存中的高度,那么不僅要更新緩存中這一項的bottom和height屬性
???????????*?在該列表項后續(xù)的所有列表項的top、bottom都會受到它的影響,所以我們又需要一層for循環(huán)進(jìn)行更改緩存中后續(xù)的值
???????????*/
????????allData[index].bottom?+=?dValue
????????allData[index].height?=?realHeight
????????/**
???????????*?注意:這里更改的一定要是緩存數(shù)組中對應(yīng)位置后續(xù)的所有值,如果只改變的是showList值的話
???????????*?會造成dataList間斷性的bottom和下一個top不連續(xù),因為startIndex、endIndex以及空白填充區(qū)域都是依據(jù)top和bottom值來進(jìn)行計算的
???????????*?所以會導(dǎo)致最后計算的結(jié)果出錯,滑動得來的startIndex變化幅度大且滾動條不穩(wěn)定,出現(xiàn)明顯抖動問題
???????????*/
????????for?(let?j?=?index?+?1,?len?=?allData.length;?j?<?len;?j++)?{
??????????allData[j].top?=?allData[j?-?1].bottom
??????????allData[j].bottom?+=?dValue
????????}
??????}
????}
??}
??//?eslint-disable-next-line
})
得到可視區(qū)域的起始和結(jié)束元素索引&&設(shè)置上下滾動緩沖區(qū)域消除快速滾動白屏
列表項的bottom屬性代表的就是該元素尾部到容器頂部的距離,不難發(fā)現(xiàn),可視區(qū)的第一個元素的bottom是第一個大于滾動高度的;可視區(qū)最后一個元素的bottom是第一個大于(滾動高度+可視高度)的。我們可以利用這條規(guī)則遍歷緩存數(shù)組找到對應(yīng)的startIndex和endIndex
由于我們的緩存數(shù)據(jù),本身就是有順序的,所以獲取開始索引的方法可以考慮通過二分查找的方式來降低檢索次數(shù),減少時間復(fù)雜度
//?得到要渲染數(shù)據(jù)的起始索引和結(jié)束索引
const?getIndex?=?()?=>?{
??//?設(shè)置緩沖區(qū)域的數(shù)據(jù)量
??const?aboveCount?=?5
??const?belowCount?=?5
??//?結(jié)果數(shù)組,里面包含了起始索引和結(jié)束索引
??const?resObj?=?{
????startIndex:?0,
????endIndex:?0,
??}
??const?scrollTop?=?containerRef.current.scrollTop
??const?dataList?=?dataListRef.current
??const?len?=?dataList.length
??//?設(shè)置上層緩沖區(qū),如果索引值大于緩沖區(qū)域,那么就需要減小startIndex的值用于設(shè)置頂層緩沖區(qū)
??const?startIndex?=?binarySearch(scrollTop)
??if?(startIndex?<=?aboveCount)?{
????resObj.startIndex?=?0
??}?else?{
????resObj.startIndex?=?startIndex?-?aboveCount
??}
??/**
?????*?緩沖數(shù)據(jù)中第一個bottom大于滾動高度加上可視區(qū)域高度的元素就是可視區(qū)域最后一個元素
?????*?如果沒有找到的話就說明當(dāng)前滾動的幅度過大,緩存中沒有數(shù)據(jù)的bottom大于我們的目標(biāo)值,所以搜索不到對應(yīng)的索引,我們只能拿緩存數(shù)據(jù)中的最后一個元素補(bǔ)充上
?????*/
??const?endIndex?=?binarySearch(scrollTop?+?curContainerHeight.current)?||?len?-?1
??//?增大endIndex的索引值用于為滾動區(qū)域下方設(shè)置一段緩沖區(qū),避免快速滾動所導(dǎo)致的白屏問題
??resObj.endIndex?=?endIndex?+?belowCount
??return?resObj
}
//?由于我們的緩存數(shù)據(jù),本身就是有順序的,所以獲取開始索引的方法可以考慮通過二分查找的方式來降低檢索次數(shù):
const?binarySearch?=?(value)?=>?{
??const?list?=?dataListRef.current
??let?start?=?0;
??let?end?=?list.length?-?1;
??let?tempIndex?=?null;
??while?(start?<=?end)?{
????let?midIndex?=?parseInt((start?+?end)?/?2);
????let?midValue?=?list[midIndex].bottom;
????if?(midValue?===?value)?{
??????//?說明當(dāng)前滾動區(qū)域加上可視區(qū)域剛好是一個結(jié)點(diǎn)的邊界,那么我們可以以其下一個結(jié)點(diǎn)作為末尾元素
??????return?midIndex?+?1;
????}?else?if?(midValue?<?value)?{
??????//?由于當(dāng)前值與目標(biāo)值還有一定的差距,所以我們需要增加start值以讓下次中點(diǎn)的落點(diǎn)更靠后
??????start?=?midIndex?+?1;
????}?else?if?(midValue?>?value)?{
??????//?因為我們的目的并不是找到第一個滿足條件的值,而是要找到滿足條件的最小索引值
??????if?(tempIndex?===?null?||?tempIndex?>?midIndex)?{
????????tempIndex?=?midIndex;
??????}
??????//?由于我們要繼續(xù)找更小的索引,所以需要讓end-1以縮小范圍,讓下次中點(diǎn)的落點(diǎn)更靠前
??????end--
????}
??}
??return?tempIndex;
}
監(jiān)聽滾動事件動態(tài)截取數(shù)據(jù)&&動態(tài)設(shè)置上下空白占位
動態(tài)截取數(shù)據(jù)的操作和定高的虛擬列表幾乎一樣,區(qū)別比較大的地方就在padding值的計算方式上。在定高的列表中,我們可以根據(jù)起始索引值和結(jié)尾索引值直接計算出空白填充區(qū)域的高度。
其實在不定高的列表中,計算方式更加簡單,因為startIndex對應(yīng)元素的top值就是我們需要填充的上空白區(qū)域,下空白區(qū)域也可以根據(jù)整個列表的高度(最后一個元素的bottom值)和endIndex對應(yīng)元素的bottom值之差得出
const?scrollHandle?=?()?=>?{
??//?獲取當(dāng)前要渲染元素的起始索引和結(jié)束索引值
??let?{?startIndex,?endIndex?}?=?getIndex()
??/**
?????*?如果是用戶滾動觸發(fā)的,而且兩次startIndex的值都一樣,那么就沒有必要執(zhí)行下面的邏輯,
?????*?除非是用戶重新請求了之后需要默認(rèn)執(zhí)行一次該函數(shù),這是一種特殊情況,就是startIndex沒變,但需要執(zhí)行后續(xù)的操作
?????*/
??if?(!isNeedLoad?&&?lastStartIndex.current?===?startIndex)?return
??//?渲染完一次之后就需要初始化isNeedLoad
??isNeedLoad.current?=?false
??//?用于實時監(jiān)控lastStartIndex的值
??lastStartIndex.current?=?startIndex
??//?下層緩沖區(qū)域最后的元素接觸到屏幕底部的時候,就可以請求發(fā)送數(shù)據(jù)了
??const?currLen?=?dataListRef.current.length
??if?(endIndex?>=?currLen?-?1)?{
????//?當(dāng)前不在請求狀態(tài)下時才可以改變請求參數(shù)發(fā)送獲取更多數(shù)據(jù)的請求
????!isRequestRef.current?&&?setOptions(state?=>?({?offset:?state.offset?+?1?}))
????//?注意endIndex不可以大于緩存中最后一個元素的索引值
????endIndex?=?currLen?-?1
??}
??//?空白填充區(qū)域的樣式
??topBlankFill.current?=?{
????//?改變空白填充區(qū)域的樣式,起始元素的top值就代表起始元素距頂部的距離,可以用來充當(dāng)paddingTop值
????paddingTop:?`${dataListRef.current[startIndex].top}px`,
????//?緩存中最后一個元素的bottom值與endIndex對應(yīng)元素的bottom值的差值可以用來充當(dāng)paddingBottom的值
????paddingBottom:?`${dataListRef.current[dataListRef.current.length?-?1].bottom?-?dataListRef.current[endIndex].bottom}px`
??}
??setShowList(dataListRef.current.slice(startIndex,?endIndex?+?1))
}
問題思考
我們雖然實現(xiàn)了根據(jù)列表項動態(tài)高度下的虛擬列表,但如果列表項中包含圖片,并且列表高度由圖片撐開。在這種場景下,由于圖片會發(fā)送網(wǎng)絡(luò)請求,列表項可能已經(jīng)渲染到頁面中了,但是圖片還沒有加載出來,此時無法保證我們在獲取列表項真實高度時圖片是否已經(jīng)加載完成,獲取到的高度有無包含圖片高度,從而造成計算不準(zhǔn)確的情況。
但是這種任意由圖片來撐開盒子大小的場景很少見,因為這樣會顯得整個列表很不規(guī)則。大多數(shù)展示圖片的列表場景,其實都是提前確定要展示圖片的尺寸的,比如微博,1張圖片的尺寸是多少,2x2,3x3的尺寸是多少都是提前設(shè)計好的,只要我們給img標(biāo)簽加了固定高度,這樣就算圖片還沒有加載出來,但是我們也能夠準(zhǔn)確的知道列表項的高度是多少。
如果你真的遇到了這種列表項會由圖片任意撐開的場景,可以給圖片綁定onload事件,等到它加載完之后再重新計算一下列表的高度,然后把它更新到緩存數(shù)據(jù)中,這是一種方法。其次,還可以使用ResizeObserver[3]來監(jiān)聽列表項內(nèi)容區(qū)域的高度改變,從而實時獲取每一列表項的高度,只不過MDN有說道這只是在實驗中的一個功能,所以暫時可能沒有辦法兼容所有的瀏覽器!
如果大家有其它更好的方法,可以在評論區(qū)交流哦!
參考資料
[1]https://juejin.cn/post/7082366494348148744:?https://juejin.cn/post/7082366494348148744
[2]https://juejin.cn/post/6844903982742110216#heading-3:?https://juejin.cn/post/6844903982742110216#heading-3
[3]https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FResizeObserver:?https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
?? ?“分享、點(diǎn)贊 、 在看” 支持一波??
