一个简洁、有趣的无限下拉方案
作者:網(wǎng)易云音樂(lè)前端團(tuán)隊(duì)
https://juejin.im/post/5de5baf2518825235b095cbe
本文主旨
長(zhǎng)列表渲染、無(wú)限下拉也算是前端開(kāi)發(fā)老生常談的問(wèn)題之一了,本文將介紹一種簡(jiǎn)潔、巧妙、高效的方式來(lái)實(shí)現(xiàn)。話不多說(shuō),看下圖,也許你可以發(fā)現(xiàn)什么?
不知你是否從上面這張圖中注意到了什么,比如只是渲染了可視區(qū)域的部分 DOM?,滾動(dòng)過(guò)程中只是外層容器的 padding 在改變?
前一點(diǎn)很好理解,我們考慮到性能,不可能將一個(gè)長(zhǎng)列表(甚至是一個(gè)無(wú)限下拉列表)的所有列表元素都進(jìn)行渲染;而后一點(diǎn),則是本文所介紹方案的核心之一!
不賣關(guān)子,提前告訴你該方案的要素就是兩個(gè):
Interp Observer
padding
說(shuō)明了要素,也許你可以嘗試著開(kāi)始思考,看你是否能猜到具體的實(shí)現(xiàn)方案。
方案介紹
Interp Observer
基本概念
一直以來(lái),檢測(cè)元素的可視狀態(tài)或者兩個(gè)元素的相對(duì)可視狀態(tài)都不是件容易事。傳統(tǒng)的各種方案不但復(fù)雜,而且性能成本很高,比如需要監(jiān)聽(tīng)滾動(dòng)事件,然后查詢 DOM , 獲取元素高度、位置,計(jì)算距離視窗高度等等。
這就是 Interp Observer 要解決的問(wèn)題。它為開(kāi)發(fā)人員提供一種便捷的新方法來(lái)異步查詢?cè)叵鄬?duì)于其他元素或視窗的位置,消除了昂貴的 DOM 查詢和樣式讀取成本。
兼容性
主要在 Safari 上兼容性較差,需要 12.2 及以上才兼容,不過(guò)還好,有 polyfill 可食用。
一些應(yīng)用場(chǎng)景
頁(yè)面滾動(dòng)時(shí)的懶加載實(shí)現(xiàn)。
無(wú)限下拉(本文的實(shí)現(xiàn))。
監(jiān)測(cè)某些廣告元素的曝光情況來(lái)做相關(guān)數(shù)據(jù)統(tǒng)計(jì)。
監(jiān)測(cè)用戶的滾動(dòng)行為是否到達(dá)了目標(biāo)位置來(lái)實(shí)現(xiàn)一些交互邏輯(比如視頻元素滾動(dòng)到隱藏位置時(shí)暫停播放)。
padding 方案實(shí)現(xiàn)
基本了解 Interp Observer 之后,接下來(lái)就看下如何用 Interp Observer + padding 來(lái)實(shí)現(xiàn)無(wú)限下拉。
先概覽下總體思路:
監(jiān)聽(tīng)一個(gè)固定長(zhǎng)度列表的首尾元素是否進(jìn)入視窗;
更新當(dāng)前頁(yè)面內(nèi)渲染的第一個(gè)元素對(duì)應(yīng)的序號(hào);
根據(jù)上述序號(hào),獲取目標(biāo)數(shù)據(jù)元素,列表內(nèi)容重新渲染成對(duì)應(yīng)內(nèi)容;
容器 padding 調(diào)整,模擬滾動(dòng)實(shí)現(xiàn)。
核心:利用父元素的 padding 去填充隨著無(wú)限下拉而本該有的、越來(lái)越多的 DOM 元素,僅僅保留視窗區(qū)域上下一定數(shù)量的 DOM 元素來(lái)進(jìn)行數(shù)據(jù)渲染。
1、監(jiān)聽(tīng)一個(gè)固定長(zhǎng)度列表的首尾元素是否進(jìn)入視窗
// 觀察者創(chuàng)建 this.observer = new InterpObserver(callback, options);// 觀察列表第一個(gè)以及最后一個(gè)元素 this.observer.observe(this.firstItem); this.observer.observe(this.lastItem); 復(fù)制代碼我們以在頁(yè)面中渲染固定的 20 個(gè)列表元素為例,我們對(duì)第一個(gè)元素和最后一個(gè)元素,用 Interp Observer 進(jìn)行觀察,當(dāng)他們其中一個(gè)重新進(jìn)入視窗時(shí),callback 函數(shù)就會(huì)觸發(fā):
const callback = (entries) => {entries.forEach((entry) => {if (entry.target.id === firstItemId) {// 當(dāng)?shù)谝粋€(gè)元素進(jìn)入視窗} else if (entry.target.id === lastItemId) {// 當(dāng)最后一個(gè)元素進(jìn)入視窗}}); }; 復(fù)制代碼2、更新當(dāng)前頁(yè)面渲染的第一個(gè)元素對(duì)應(yīng)的序號(hào) (firstIndex)
拿具體例子來(lái)說(shuō)明,我們用一個(gè)數(shù)組來(lái)維護(hù)需要渲染到頁(yè)面中的數(shù)據(jù)。數(shù)組的長(zhǎng)度會(huì)隨著不斷請(qǐng)求新的數(shù)據(jù)而不斷變大,而渲染的始終是其中一定數(shù)量的元素,比如 20 個(gè)。那么:
1、最開(kāi)始渲染的是數(shù)組中序號(hào)為 0 - 19 的元素,即此時(shí)對(duì)應(yīng)的 firstIndex 為 0;
2、當(dāng)序號(hào)為 19 的元素(即上一步的 lastItem )進(jìn)入視窗時(shí),我們就會(huì)往后渲染 10 個(gè)元素,即渲染序號(hào)為 10 - 29 的元素,那么此時(shí)的 firstIndex 為 10;
3、下一次就是,當(dāng)序號(hào)為 29 的元素進(jìn)入視窗時(shí),繼續(xù)往后渲染 10個(gè)元素,即渲染序號(hào)為 20 - 39 的元素,那么此時(shí)的 firstIndex 為 20,以此類推。。。
總體來(lái)說(shuō),更新 firstIndex,是為了根據(jù)頁(yè)面的滾動(dòng)情況,知道接下來(lái)哪些數(shù)據(jù)應(yīng)該被獲取、渲染。
3、根據(jù)上述序號(hào),獲取對(duì)應(yīng)數(shù)據(jù)元素,列表重新渲染成新的內(nèi)容
const renderFunction = (firstIndex) => {// offset = firstIndex, limit = 10 => getData// getData Done => new dataItems => render DOM}; 復(fù)制代碼這一部分就是根據(jù) firstIndex 查詢數(shù)據(jù),然后將目標(biāo)數(shù)據(jù)渲染到頁(yè)面上即可。
4、padding 調(diào)整,模擬滾動(dòng)實(shí)現(xiàn)
既然數(shù)據(jù)的更新以及 DOM 元素的更新我們已經(jīng)實(shí)現(xiàn)了,那么無(wú)限下拉的效果以及滾動(dòng)的體驗(yàn),我們要如何實(shí)現(xiàn)呢?
想象一下,拋開(kāi)一切,最原始最直接最粗暴的方式無(wú)非就是我們?cè)儆肢@取了 10 個(gè)新的數(shù)據(jù)元素之后,再塞 10 個(gè)新的 DOM 元素到頁(yè)面中去來(lái)渲染這些數(shù)據(jù)。
但此時(shí),對(duì)比上面這個(gè)粗暴的方案,我們的方案是:這 10個(gè)新的數(shù)據(jù)元素,我們用原來(lái)已有的 DOM 元素去渲染,替換掉已經(jīng)離開(kāi)視窗、不可見(jiàn)的數(shù)據(jù)元素;而本該由更多 DOM 元素進(jìn)一步撐開(kāi)容器高度的部分,我們用 padding 填充來(lái)模擬實(shí)現(xiàn)。
向下滾動(dòng)
向上滾動(dòng)
最后是 padding 設(shè)置更新以及相關(guān)緩存數(shù)據(jù)更新
思考總結(jié)
方案總結(jié):
利用 Interp Observer 來(lái)監(jiān)測(cè)相關(guān)元素的滾動(dòng)位置,異步監(jiān)聽(tīng),盡可能得減少 DOM 操作,觸發(fā)回調(diào),然后去獲取新的數(shù)據(jù)來(lái)更新頁(yè)面元素,并且用調(diào)整容器 padding 來(lái)替代了本該越來(lái)越多的 DOM 元素,最終實(shí)現(xiàn)列表滾動(dòng)、無(wú)限下拉。
相關(guān)方案的對(duì)比
這里和較為有名的庫(kù) - iScroll 實(shí)現(xiàn)的無(wú)限下拉方案進(jìn)行一個(gè)基本的對(duì)比,對(duì)比之前先說(shuō)明下 iScroll infinite 的實(shí)現(xiàn)概要:
iScroll 通過(guò)對(duì)傳統(tǒng)滾動(dòng)事件的監(jiān)聽(tīng),獲取滾動(dòng)距離,然后:
設(shè)置父元素的 translate 來(lái)實(shí)現(xiàn)整體內(nèi)容的上移(下移);
再基于這個(gè)滾動(dòng)距離進(jìn)行相應(yīng)計(jì)算,得知相應(yīng)子元素已經(jīng)被滾動(dòng)到視窗外,并且判斷是否應(yīng)該將這些離開(kāi)視窗的子元素移動(dòng)到末尾,從而再對(duì)它們進(jìn)行 translate 的設(shè)置來(lái)移動(dòng)到末尾。這就像是一個(gè)循環(huán)隊(duì)列一樣,隨著滾動(dòng)的進(jìn)行,頂部元素先出視窗,但又將移動(dòng)到末尾,從而實(shí)現(xiàn)無(wú)限下拉。
相關(guān)對(duì)比:
實(shí)現(xiàn)對(duì)比:一個(gè)是 Interp Observer 的監(jiān)聽(tīng),來(lái)通知子元素離開(kāi)視窗,只要定量設(shè)置父元素 padding 就行;另一個(gè)是對(duì)傳統(tǒng)滾動(dòng)事件的監(jiān)聽(tīng),滾動(dòng)距離的獲取,再進(jìn)行一系列計(jì)算,去設(shè)置父元素以及子元素的 translate。顯而易見(jiàn),前者看起來(lái)更加簡(jiǎn)潔明了一些。
性能對(duì)比:我知道說(shuō)到對(duì)比,你腦海中肯定一下子會(huì)想到性能問(wèn)題。其實(shí)性能對(duì)比的關(guān)鍵就是 Interp Observer。因?yàn)閱尉?padding 設(shè)置還是 translate 設(shè)置,性能方面的差距是甚小的,只是個(gè)人感覺(jué) padding 會(huì)簡(jiǎn)潔些?而 Interp Observer 其實(shí)抽離了所有滾動(dòng)層面的相關(guān)邏輯,你不再需要對(duì)滾動(dòng)距離等相應(yīng) DOM 屬性進(jìn)行獲取,也不再需要進(jìn)行一系列滾動(dòng)距離相關(guān)的復(fù)雜計(jì)算,并且同步的滾動(dòng)事件觸發(fā)變成異步的,你也不再需要另外去做防抖之類的邏輯,這在性能方面還是有所提升的。
存在的缺陷:
padding 的計(jì)算依賴列表項(xiàng)固定的高度。
這是一個(gè)同步渲染的方案,也就是目前容器 padding 的計(jì)算調(diào)整,無(wú)法計(jì)算異步獲取的數(shù)據(jù),只跟用戶的滾動(dòng)行為有關(guān)。這看起來(lái)與實(shí)際業(yè)務(wù)場(chǎng)景有些不符。解決思路:
思路 1、利用 Skeleton Screen Loading 來(lái)同步渲染數(shù)據(jù)元素,不受數(shù)據(jù)異步獲取的影響。即在數(shù)據(jù)請(qǐng)求還未完成時(shí),先使用一些圖片進(jìn)行占位,待內(nèi)容加載完成之后再進(jìn)行替換。
思路 2、滾動(dòng)到目標(biāo)位置,阻塞容器 padding 的設(shè)置(即無(wú)限下拉的發(fā)生)直至數(shù)據(jù)請(qǐng)求完畢,用 loading gif 提示用戶加載狀態(tài),但這個(gè)方案相對(duì)復(fù)雜,你需要全面考慮用戶難以預(yù)測(cè)的滾動(dòng)行為來(lái)設(shè)置容器的 padding。
延伸拓展
請(qǐng)大家思考一下,無(wú)限下拉有了,那么無(wú)限上拉基于這種方案要如何調(diào)整實(shí)現(xiàn)呢?
如果將 Interp Observer 用到 iScroll 里面去,原有方案可以怎樣優(yōu)化?
代碼實(shí)現(xiàn)
完整代碼實(shí)現(xiàn)參考(https://github.com/Guohjia/listScroll)
參考文章
Interp Observer API
InterpObserver’s Coming into View
Infinite Scroll’ing the right way
總結(jié)
以上是生活随笔為你收集整理的一个简洁、有趣的无限下拉方案的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Flutter 1.17 | 2020
- 下一篇: 学会这些Python美图技巧,就等着女朋