【W(wǎng)eb技術(shù)】1206- 如何設(shè)計一款支持懶加載的瀑布流組件?
前言
瀑布流布局算是一種比較流行的布局,參差不齊的多列結(jié)構(gòu),不僅能節(jié)省空間,還能在視覺展示上錯落有致不拘一格。在一次業(yè)務(wù)需求中,找了幾個開源的瀑布流組件,在使用的過程中總會有點小問題,便開發(fā)了此組件。
在開始之前你可能需要先了解一下IntersectionObserver[1],核心是這個API監(jiān)聽指定的卡片是否在可視區(qū)域展示,當(dāng)一個被監(jiān)聽卡片出現(xiàn)在可視區(qū)域,就會觸發(fā)回調(diào),執(zhí)行列于列之間對比邏輯,并在高度較小的列添加數(shù)據(jù)。
基本使用
這款組件已經(jīng)上傳npm[2]了,有興趣的小伙伴可以下載使用一下。
1、安裝
npm?i?waterfall-vue2
2、使用方式
import?{?Waterfall?}?from?"waterfall-vue2";
Vue.use(Waterfall);
<Waterfall
??:pageData="pageData"
??:columnCount="2"
??:colStyle="{display:'flex',flexDirection:'column',alignItems:'center'}"
??query-sign="#cardItem"
??@wfLoad="onLoad"
??@ObserveDataTotal="ObserveDataTotal"
>
??<template?#default="{?item,?columnIndex,?index}">
????
????<good-card?:item="item"?id="cardItem"?/>
??template>
Waterfall>
3、基本參數(shù)和事件
API
| 參數(shù) | 說明 | 類型 | 默認值 | 版本 |
|---|---|---|---|---|
| columnCount | 列數(shù) | Number | 2 | - |
| pageData | 當(dāng)前 pageIndex 請求的數(shù)據(jù)(非多頁累加數(shù)據(jù)) | Array | [] | - |
| resetSign | 重置數(shù)據(jù)(清空每列數(shù)據(jù)) | Boolean | false | - |
| immediateCheck | 立即檢查 | Boolean | true | - |
| offset | 觸發(fā)加載的距離閾值,單位為px | String|Number | 300 | - |
| colStyle | 每列的樣式 | Object | {} | - |
| querySign | 內(nèi)容標(biāo)識(querySelectorAll選擇器) | String | 必須項 | - |
Event
| 事件名 | 說明 | 參數(shù) |
|---|---|---|
| wfLoad | 滾動條與底部距離小于 offset 時觸發(fā) | - |
| ObserveDataTotal | 未渲染的數(shù)據(jù)總數(shù) | length |
Slot
| 名稱 | 說明 |
|---|---|
| default | 插槽內(nèi)容 |
| columnIndex | 當(dāng)前內(nèi)容所在的列 |
| item | 單條數(shù)據(jù) |
| index | 當(dāng)前數(shù)據(jù)所在列的下標(biāo) |
技術(shù)實現(xiàn)
難點
圖片大多數(shù)會使用懶加載實現(xiàn),一般情況下節(jié)點內(nèi)容已加載完成,圖片未出現(xiàn)在可視區(qū)域內(nèi)未加載。在圖片高度不確定的情況下,怎么確保列表中列于列的落差小于一個卡片內(nèi)容高度呢?
原理
利用IntersectionObserver監(jiān)聽固定的節(jié)點信息,每當(dāng)監(jiān)聽節(jié)點出現(xiàn)在可視區(qū)域中,就會觸發(fā)IntersectionObserver回調(diào),在回調(diào)中執(zhí)行數(shù)據(jù)插入,對比每列的內(nèi)容的高度,在監(jiān)聽數(shù)據(jù)池中取出一個數(shù)據(jù)放在最小列高度的數(shù)據(jù)列表中。每一個數(shù)據(jù)卡片的展示,就會觸發(fā)新數(shù)據(jù)卡片的加載,這就是「懶加載」瀑布流組件的核心思想。
如下圖,當(dāng)卡片7剛剛展示在可視區(qū)域的時候,就會觸發(fā)IntersectionObserver回調(diào),再回調(diào)邏輯中執(zhí)行插入函數(shù),插入函數(shù)中進行列于列之間的對比,此時對比發(fā)現(xiàn)B列高度較小,然后在監(jiān)聽數(shù)據(jù)池中取出一個數(shù)據(jù),放入B列的數(shù)據(jù)列表中,渲染出卡片8。

設(shè)計規(guī)劃
1、一般瀑布流排列方式主要可以分為兩種,一是分欄布局,一是絕對定位布局。不管那種思想難點都在于解決圖片動態(tài)高度的問題。本次采用分欄布局方式,這樣能夠減少因圖片加載而進行大面積卡片位置的計算。
2、創(chuàng)建一個數(shù)據(jù)監(jiān)聽池,作用一是將所有未渲染的數(shù)據(jù)保存在其中。作用二是當(dāng)數(shù)據(jù)池的數(shù)據(jù)取完之后可以減少很多不必要的執(zhí)行操作。
3、參數(shù)規(guī)劃
//?列數(shù)
????columnCount:?{
??????type:?Number,
??????default:?2,
????},
????//?每頁數(shù)據(jù)
????pageData:?{
??????type:?Array,
??????default:?()?=>?[],
????},
????//?重置
????resetSign:?{
??????type:?Boolean,
??????default:?false,
????},
????//?立即檢查
????immediateCheck:?{
??????type:?Boolean,
??????default:?true,
????},
????//?偏移
????offset:?{
??????type:?[Number,?String],
??????default:?300,
????},
????//?樣式
????colStyle:?{
??????type:?Object,
??????default:?()?=>?({}),
????},
????//?查詢標(biāo)識
????querySign:?{
??????type:?String,
??????require:?true,
????},
3、函數(shù)規(guī)劃
getMinColSign 返回最小列的標(biāo)識 checkObserveDom 檢查當(dāng)前dom是否有未監(jiān)聽,將未監(jiān)聽的節(jié)點放入監(jiān)聽范圍內(nèi) insetData 執(zhí)行取數(shù)據(jù)并插入列數(shù)據(jù)中 getScrollParentNode 獲取祖先滾動元素,并綁定滾動事件 check 滾動檢查是否觸發(fā)加載閥值
4、流程圖設(shè)計

實踐
pageData實現(xiàn)
傳入數(shù)據(jù),放入監(jiān)視數(shù)據(jù)池 如重置標(biāo)識為true,清空監(jiān)視數(shù)據(jù)、列數(shù)據(jù), 每次新數(shù)據(jù)都會觸發(fā)數(shù)據(jù)插入 如果不兼容IntersectionObserver,每列均分當(dāng)前的數(shù)據(jù)
???pageData(value?=?[])?{
??????if?(!value.length)?return
??????if?(IntersectionObserver)?{
????????//?判斷當(dāng)前是否需要重置
????????if?(this.resetSign)?{
??????????//?重置斷開當(dāng)前全部監(jiān)控數(shù)據(jù)
??????????this.intersectionObserve.disconnect()
??????????Object.keys(this.colListData).forEach((key)?=>?{
????????????this.colListData[key]?=?[]
??????????})
??????????this.observeData?=?[...value]
??????????this.$nextTick(()?=>?{
????????????this.insetData()
??????????})
????????}?else?{
??????????this.observeData?=?[...this.observeData,?...value]
??????????//?插入數(shù)據(jù)
??????????this.insetData()
????????}
??????}?else?{
????????//?當(dāng)?IntersectionObserver?不支持,每列數(shù)據(jù)均勻分配
????????const?val?=?(this.observeData?=?value)
????????while?(Array.isArray(val)?&&?val.length)?{
??????????let?keys?=?null
??????????//?盡量減小數(shù)據(jù)分配不均勻
??????????if?(this.averageSign)?{
????????????keys?=?Object.keys(this.colListData)
??????????}?else?{
????????????keys?=?Object.keys(this.colListData).reverse()
??????????}
??????????keys.forEach((key)?=>?{
????????????const?item?=?val.shift()
????????????item?&&?this.colListData[key].push(item)
??????????})
??????????this.averageSign?=?!this.averageSign
????????}
??????}
????}
insetData實現(xiàn)數(shù)據(jù)插入函數(shù),確保控制數(shù)據(jù)的入口只有一個,避免同一批處理周期內(nèi)執(zhí)行多次。
//?插入數(shù)據(jù)
????insetData()?{
??????const?sign?=?this.getMinColSign()
??????const?divData?=?this.observeData?&&?this.observeData.shift()
??????if?(!divData?||?!sign)?{
????????return?null
??????}
??????this.colListData[sign].push(divData)
??????this.checkObserveDom()
????},
getMinColSign實現(xiàn)獲取當(dāng)前所有列中高度最小的列,并返回其標(biāo)識
//?獲取最小高度最小的標(biāo)識
????getMinColSign()?{
??????let?minHeight?=?-1
??????let?sign?=?null
??????Object.keys(this.colListData).forEach((key)?=>?{
????????const?div?=?this.$refs[key][0]
????????if?(div)?{
??????????const?height?=?div.offsetHeight
??????????if?(minHeight?===?-1?||?minHeight?>?height)?{
????????????minHeight?=?height
????????????sign?=?key
??????????}
????????}
??????})
??????return?sign
????},
checkObserveDom實現(xiàn)將未加入監(jiān)視的節(jié)點,加入監(jiān)視
//?檢查dom是否全部被監(jiān)控
????checkObserveDom()?{
??????const?divs?=?document.querySelectorAll(this.querySign)
??????if?(!divs?||?divs.length?===?0)?{
????????//?防止數(shù)據(jù)插入dom未渲染,監(jiān)聽函數(shù)無數(shù)據(jù)
????????setTimeout(()?=>?{
??????????//?每次新數(shù)據(jù)的首個數(shù)據(jù)無法監(jiān)控,需要延遲觸發(fā)
??????????this.insetData()
????????},?100)
??????}
??????divs.forEach((div)?=>?{
????????if?(!div.getAttribute('data-intersectionobserve'))?{
??????????//?避免重復(fù)監(jiān)聽
??????????this.intersectionObserve.observe(div)
??????????div.setAttribute('data-intersectionobserve',?true)
????????}
??????})
????}
observeData實現(xiàn)
每次數(shù)據(jù)池數(shù)據(jù)去空修改觸底標(biāo)識,只要是防止?jié)L動持續(xù)觸底,當(dāng)前數(shù)據(jù)未渲染完 首次數(shù)據(jù)取空查找祖先滾動元素 每次數(shù)據(jù)變化,發(fā)布事件,告知當(dāng)前數(shù)據(jù)池剩余數(shù)據(jù)
??observeData(val)?{
??????if(!val)?return
??????if?(val.length?===?0)?{
????????if?(this.onceSign)?{
??????????//?監(jiān)視數(shù)組數(shù)據(jù)分發(fā)完了,在進行首次的祖先滾動元素的查找
??????????this.onceSign?=?false
??????????this.scrollTarget?=?this.getScrollParentNode(this.$el)
??????????this.scrollTarget.addEventListener('scroll',?this.check)
????????}
????????//?數(shù)據(jù)更新,修改觸發(fā)觸底標(biāo)識
????????this.emitSign?=?true
??????}
??????this.$emit('ObserveDataTotal',?val.length)
????}
getScrollParentNode實現(xiàn)在內(nèi)容未加載的時候,無法準(zhǔn)確的通過overflow屬性查找到滾動祖先元素,為了能夠更準(zhǔn)確的獲取祖先滾動元素,在首次內(nèi)容全部加載之后才進行祖先滾動元素的查找
//?獲取滾動的父級元素
????getScrollParentNode(el)?{
??????let?node?=?el
??????while?(node.nodeName?!==?'HTML'?&&?node.nodeName?!==?'BODY'?&&?node.nodeType?===?1)?{
????????const?parentNode?=?node.parentNode
????????const?{?overflowY?}?=?window.getComputedStyle(parentNode)
????????if?(
??????????(overflowY?===?'scroll'?||?overflowY?===?'auto')?&&
??????????parentNode.clientHeight?!=?parentNode.scrollHeight
????????)?{
??????????return?parentNode
????????}
????????node?=?parentNode
??????}
??????return?window
????},
check實現(xiàn)檢查是否觸發(fā)load
//?滾動檢查
????check()?{
??????this.intersectionObserve?&&?this.checkObserveDom()
??????//?觸底標(biāo)識為false直接跳過
??????if?(!this.emitSign)?{
????????return
??????}
??????const?{?scrollTarget?}?=?this
??????let?bounding?=?{
????????top:?0,
????????bottom:?scrollTarget.innerHeight?||?0,
??????}
??????if?(this.$refs.bottom.getBoundingClientRect)?{
????????bounding?=?this.$refs.bottom.getBoundingClientRect()
??????}
??????//?元素所在視口容器的高度
??????let?height?=?bounding.bottom?-?bounding.top
??????if?(!height)?{
????????return
??????}
??????const?container?=?scrollTarget.innerHeight?||?scrollTarget.clientHeight
??????const?distance?=?bounding.bottom?-?container?-?this._offset
??????if?(distance?0)?{
????????//?發(fā)布事件
????????this.$emit('wfLoad')
????????//?發(fā)布事件觸發(fā)修改觸底標(biāo)識
????????this.emitSign?=?false
??????}
????},
最后
上面便是整個「懶加載」瀑布流組件的產(chǎn)生過程,感興趣的小伙伴可以下載使用,體驗一下?;蛘吣懈玫南敕ǎ嗷ヌ接?,共同進步。
源碼地址
github:https://github.com/zengxiangfu/vue2-waterfall
參考資料
IntersectionObserver: https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
[2]npm: https://www.npmjs.com/package/waterfall-vue2

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點擊“閱讀原文”查看 130+ 篇原創(chuàng)文章
