瀑布流算是比較常見的佈局了,一個般常見縱向瀑布流的互動,當我們捲動到底的時候載入下一頁的資料追加到上去。因為一次載入的資料量不是很多,頁面操作是也不會有太大的效能消耗。但是如果當你一直往下捲動載入,載入幾十頁的時候,就會開始感覺不那麼流暢的,這是因為雖然每次操作的很少,但是頁面的 DOM 越來越多,記憶體佔用也會增大,而且發生重排重繪時候瀏覽器計算量耗時也會增大,就導致了慢慢不能那麼流暢了。這個時候可以選擇結合虛擬列表方式使用,虛擬列表本身就是用來解決超長列表時的處理方案。
瀑布流的實現方式有很多種,大體分為:
因為我的瀑布流是可提前計算元素寬高,列數是動態的,所以採用了 JavaScript + position 來配合 虛擬列表 進行優化。
如果你的瀑布流 列是固定,列寬不固定 的,使用 flex 是個很好選擇,當你的容器寬度變話時候,每一列寬度會自適應,大致實現方式
將你的資料分為對應列數
let data1 = [], //第一列
data2 = [], //第二列
data3 = [], //第三列
i = 0;
while (i < data.length) {
data1.push(data[i++]);
if (i < data.length) {
data2.push(data[i++]);
}
if (i < data.length) {
data3.push(data[i++]);
}
}
然後將你的每列資料插入進去就可以了,設定 list 為 flex 容器,並設定主軸方向為 row
<div class="list">
<!-- 第一列 -->
<div class="column">
<div class="item"></div>
<!-- more items-->
</div>
<!-- 第二列 -->
<div class="column">
<div class="item"></div>
<!-- more items-->
</div>
<!-- 第三列 -->
<div class="column">
<div class="item"></div>
<!-- more items-->
</div>
</div>
這種方式比較適合 列定寬,列數量不固定情況,而且最好能計算出每個元素的大小。
大致 HTML 結構如下:
<ul class="list">
<li class="list-item"></li>
<!-- more items-->
</ui>
<style>
.list {
position: relative;
}
.list-item {
position: absolute;
top: 0;
left: 0;
}
</style>
JavaScript 部分,首先需要獲取 list 寬度,根據 list.width/列寬 計算出列的數量,然後根據列數量去分組資料和計算位置
// 以列寬為300 間隔為20 為例
let catchColumn = (Math.max(parseInt((dom.clientWidth + 20) / (300 + 20)), 1))
const toTwoDimensionalArray = (count) => {
let list = []
for (let index = 0; index < count; index++) {
list.push([])
}
return list;
}
const minValIndex = (arr = []) => {
let val = Math.min(...arr);
return arr.findIndex(i => i === val)
}
// 快取累計高度
let sumHeight = toTwoDimensionalArray(catchColumn)
data.forEach(item => {
// 獲取累計高度最小那列
const minIndex = minValIndex(sumHeight)
let width = 0 // 這裡寬高更具需求計算出來
let height = 0
item._top = minIndex * (300 + 20) // 快取位置資訊,後面會用到
item.style = {
width: width + 'px',
height: height + 'px',
// 計算偏移位置
transform: `translate(${minIndex * (300 + 20)}px, ${sumHeight[minIndex]}px)`
}
sumHeight[minIndex] = sumHeight[minIndex] + height + 20
})
可以使用 ResizeObserver(現代瀏覽器相容比較好了) 監聽容器元素大小變化,當寬度變化時重新計算列數量,當列數量發生變化時重新計算每項的位置資訊。
const observer = debounce((e) => {
const column = updateVisibleContainerInfo(visibleContainer)
if (column !== catchColumn) {
catchColumn = column
// 重新計算
this.resetLayout()
}
}, 300)
const resizeObserver = new ResizeObserver(e => observer(e));
// 開始監聽
resizeObserver.observe(dom);
當列數量發生變化時候,元素項的位置很多都會發生變化,如下圖,第 4 項的位置從第 3 列變到了第 4 項,如果不做處理會顯得比較僵硬。
好在我們使用了 transform(也是為什麼使用 top、left 原因,transform 動畫效能更高) 進行位置偏移,可以直接使用 transition 過渡。
.list-item {
position: absolute;
top: 0;
left: 0;
transition: transform .5s ease-in-out;
}
很多虛擬列表的都是使用的單列定高使用方式,但是瀑布流使用虛擬列表方式有點不同,瀑布流存在多列且時是錯位的。所以常規 length*height 為列表總高度,根據 scrollTop/height 來確定下標方式就行不通了,這個時候高度需要根據瀑布流高度動態決定了,可顯示元素也不能通過 starindex-endindex 去擷取顯示了。
如下圖:藍色框的元素是不應該顯示的,只有與可視區域存在交叉的元素才應該顯示
先來看下面圖,當元素完全不在可視區域時候就視為當前元素不需要顯示,只有與可視區域存在交叉或被包含時候視為需要顯示。
因為上面瀑布流的實現採用的是 position 定位的,所以我們完全能知道所有元素距離頂部的距離,很容易計算出與可視區域交叉位置。
元素偏移位置 < 捲動高度+可視區域高度 && 元素偏移位置 + 元素高度 > 捲動高度
如果只渲染可視區域範圍,捲動時候會存在白屏再出現,可視適當的擴大渲染區域,例如把上一屏和下一屏都算進來,進行預先渲染。
const top = scrollTop - clientHeight
const bottom = scrollTop + clientHeight * 2
const visibleList = data.filter(item => item._top + item.height > top && item._top < bottom)
然後通過監聽捲動事件,根據捲動位置去處理篩選數。這裡會存在一個隱藏效能問題,當捲動載入資料比較多的時候,捲動事件觸發也是比較快的,每一次都進行一次遍歷,也是比較消耗效能的。可以適當控制一下事件觸發頻率,當然這也只是治標不治本,歸根倒是查詢顯示元素方法問題。
標記下標
應為列表資料的 _top 值是從小到大正序的,所以我們可以標記在可視區元素的下標,當發生捲動的時候,我們直接從標記下標開始查詢,根據捲動分幾種情況來判斷。
1> 如果捲動後,標記下標元素還在可視範圍內,可以直接從標記下標二分查詢,往上往下找直到不符合條件就停止。
2> 如果捲動後,標記下標元素不在可視範圍內,根據捲動方向往上或者往下去查詢,然後更新下標值。這個時候存在一種情況,就是當用戶拖動卷軸捲動幅度特別大的時候,可以將下標往上或者往下偏移,偏移量根據 捲動高度/預估平均高度*列數 去估算一個,然後在跟新這個預估下標進行查詢。
我們 absolute 定位會撐開容器高度,但是捲動時候還是會存在抖動問題,我們可以自定義一個元素高度去撐開,這個元素高度也就是我們之前計算的每一列累計高度 sumHeight 中最大的那個了。
當列寬發生變化時,元素位置發生了變化,在可視區域的元素也發生了變化,有些元素可能之前並沒有渲染,所以使用上面 CSS 會存在新出現元素不會產生過渡動畫。好在我們能夠很清楚的知道元素原位置資訊和新的位置資訊,我們可以利用 FLIP 來處理這動畫,很容易控制元素過渡變化,如果有些元素之前不存在,就沒有原位置資訊,我們可以在可視範圍內給他隨機生成一個位置進行過渡,保證每一個元素都有個過渡效果避免僵硬。
上面情況僅僅是針對動態列數量,又能計算出高度情況下優化,可能業務中也是可能存在每項高度是動態的,這個時候可以採用預估元素高度在渲染後快取大小位置等資訊,或者離屏渲染等方案解決做出進一步的優化處理。