常見的瀑布流實現大部分只適用於子塊尺寸固定或內部有圖片異步加載的狀況。html
而對於子塊有圖片這種可能引發尺寸變化的狀況,一般的作法是寫死圖片高度,或檢測內部的 img
元素從而在 onload
事件中進行重排。vue
因爲咱們業務中尺寸變化狀況更爲複雜,如子塊自己異步初始化、內部數據異步獲取,且這種尺寸變化時機不可肯定,爲知足這種需求因此調研完成了一個通用萬能的瀑布流實現。git
如下代碼部分以 Vue.js 爲例,思路和機制是通用的。github
先不考慮子塊尺寸變化的因素,完成基礎的瀑布流佈局功能。瀏覽器
瀑布流佈局的配置有三個,列數 columnCount
,塊水平間距 gutterWidth
、塊垂直間距 gutterHeight
。異步
固然也但是使用列寬代替列數,但一般狀況下,這樣就要求使用方進行列寬計算,有更高的使用成本
props: { columnCount: Number, gutterWidth: Number, gutterHeight: Number, }
對於類列表的結構,在組件開發中一般由兩種形式:佈局
slot
組件內循環 slot
的方式以下:字體
// Waterfall.vue <template> <div> <slot v-for="data in list" v-bind="data"> </div> </template> // 使用方--父級組件 <waterfall :list="list"> <template v-slot="data"> <ecology-card :ecology-info="data" /> </template> </waterfall>
其實現思路是,使用者將列表數據傳入組件,組件內部循環出對應個數的 slot
,並將每一項數據傳入 slot
,使用方根據傳回的數據進行自定義渲染。優化
這種方式使用起來比較違反視覺直覺,在使用者角度,不能直接的感覺到循環結構,但開發角度,邏輯更封閉,實現複雜邏輯更爲簡便。this
因爲瀑布流組件只提供佈局功能,應提供更直觀的視覺感覺,同時在咱們的業務需求中,子塊部分不盡相同,須要更靈活的自定義子塊內容的方式。
因此採起第二種實現方式,拆分設計爲 Waterfall.vue
瀑布流容器和 WaterfallItem.vue
瀑布流子塊兩個組件。
// 使用方 <waterfall> <waterfall-item> <a-widget /> // 業務組件 </waterfall-item> <waterfall-item> <b-image /> // 業務組件 </waterfall-item> </waterfall>
// Waterfall.vue <script> render (h) { return h('div', this.$slots.default) } <script> <style> .waterfall { position: relative; width: 100%; min-height: 100%; overflow-x: hidden; } </style>
Waterfall.vue
組件只須要與父組件同寬高,而且將插入內部的元素原樣渲染。
爲了保證在新增或刪除子塊時使從新佈局的成本最小化,我選擇由 WaterfallItem.vue
告知 Waterfall.vue
本身的新增和移除。
// Waterfall.vue data () { return { children: [] } }, methods: { add (child) { const index = this.$children.indexOf(child) this.children[index] = child this.resize(index, true) }, delete (child) { const index = this.$children.indexOf(child) this.children[index].splice(index, 1) this.resize(index, false) } } // WaterfallItem.vue created () { this.$parent.add(this) }, destoryed () { this.$parent.delete(this) }
那麼下面就要開始進行佈局邏輯方法的編寫。
瀑布流佈局受兩個因素影響,每一個子塊的寬和高,咱們須要在適當的時候從新獲取這兩個維度的數據,其中塊寬即列寬。
列寬受兩個因素的影響,容器寬度和指望的列數,那麼列寬明顯就是一個計算屬性,而容器寬度須要在初始化和窗口變化時從新獲取。
// Waterfall.vue data () { return { // ... containerWidth: 0 } }, computed: { colWidth () { return (this.containerWidth - this.gutterWidth * (cols -1))/this.cols } }, methods: { //... getContainerWidth () { this.containerWidth = this.$el.clientWidth } }, mounted () { this.getContainerWidth() window.addEventListener('resize', this.getContainerWidth) }, destory () { window.removeEventListener('resize', this.getContainerWidth) }
也不要忘記在組件銷燬時移除監聽。
子塊高的獲取時機有兩個:獲取新增的塊的高度和列寬變化時從新獲取全部。
data () { return { //... childrenHeights: [] } }, resize (index, update) { this.$nextTick(() => { if (!update) { this.childrenHeights.splice(index, 1) } else { const childrenHeights = this.childrenHeights.slice(0, index) for (let i = index; i < this.children.length; i++) { childrenHeights.push(this.$children[i].$el.getBoundingClientRect().height) } this.childrenHeights = childrenHeights } }) }, watch: { colWidth () { this.resize(0, true) } }
$nextTick
等待 DOM 的實際渲染,從而能夠得到尺寸。佈局思路以下:
0
,那麼取塊最少的列爲目標列,由於可能塊高爲 0
,塊垂直間距爲 0
,致使一直向第一列添加塊。// Waterfall.vue computed: { //... layouts () { const colHeights = new Array(this.columnCount).fill(0) const colItemCounts = new Array(this.columnCount).fill(0) const positions = [] this.childrenHeights.forEach(height => { let col, left, top const minHeightCol = colHeights.indexOf(min(colHeights)) const minCountCol = colItemCounts.indexOf(min(colItemCounts)) if (colHeights[minHeightCol] === 0) { col = minCountCol top = 0 } else { col = minHeightCol top = colHeights[col] + this.gutterHeight } colHeights[col] = top + height colItemCounts[col] += 1 left = (this.colWidth + this.gutterWidth) * col positions.push({ left, top }) }) const totalHeight = max(colHeights) return { positions, totalHeight } }, positions () { return this.layouts.positions || [] }, totalHeight () { return this.layouts.totalHeight || 0 } }
同時須要注意的一點是,在整個佈局的高度發生改變的時候,可能會伴隨着滾動條的出現和消失,這會引發佈局區域寬度變化,因此須要對 totalHeight
增長監聽。
watch: { totalHeight () { this.$nextTick(() => { this.getContainerWidth() }) } }
當 totalHeight
發生變化時,從新獲取容器寬度,這也是爲何 getContainerWidth
方法中使用 clientWidth
值的緣由,由於 clientWidth
不包含滾動條的寬度。
同時在 totalHeight
發生改變後要使用 $nextTick
後獲取寬度,由於 totalHeight
是咱們的計算值,此刻,佈局數據變化引起的視圖渲染還未發生,在 $nextTick
回調等待視圖渲染更新完成,再獲取 clientWidth
。
同時咱們也不須要關注 totalHeight(newValue, oldValue)
中 newValue
和 oldValue
是否相等,來而避免後續計算,由於若相等是不會觸發 totalHeight
的 watch
行爲的。
同理,也不須要判斷 totalHeight
變化先後 clientWidth
是否一致來決定是否要對 containerWidth
從新賦值,從而避免引起後續的列寬、佈局計算,由於 Vue.js 內都作了優化,只需從新獲取並賦值,避免無用的「優化」代碼。
計算完成的位置和列寬須要應用到 WaterfallItem.vue
上
<template> <div class="waterfall-item" :style="itemStyle"> <slot /> </div> </template> <script> export default { created () { this.$parent.add(this) }, computed: { itemStyle () { const index = this.$parent.$children.indexOf(this) const { left, top } = this.$parent.positions[index] || {} const width = this.$parent.colWidth return { transform: `translate3d(${left}px,${top}px,0)`, width: `${width}px` } } }, destoryed () { this.$parent.delete(this) } } </script> <style> .waterfall-item { box-sizing: border-box; border: 1px solid black; position: absolute; left: 0; right: 0; } </style>
至此,基礎瀑布流邏輯也就結束了,使用現代瀏覽器點此預覽
預覽中定時向 Waterfall 中插入高度隨機的 WaterfallItem。
完成限定子塊高度在初始渲染時就固定的瀑布流後,怎麼能作一個不管何時子塊尺寸變化,都能進行感知並從新佈局的瀑布流呢?
根據這篇文章知,能夠利用滾動事件去探知元素的尺寸變化。
簡要來講:
以 scrollTop
爲例,在滾動方向爲向右和向下,已經滾動到 scrollTop
最大值前提下
當內容(子元素)高度固定且大於容器時
scrollTop
變小觸發滾動。scrollTop
不變,不觸發滾動。當內容爲 200% 的容器尺寸時
scrollTop
不變。scrollTop
變小觸發滾動。因此咱們可使用:
那麼 WaterfallItem.vue
須要調整以下
<template> <div class="waterfall-item" :style="itemStyle"> <div class="waterfall-item__shadow" ref="bigger" @scroll="sizeChange"> <div class="waterfall-item__holder--bigger"> </div> </div> <div class="waterfall-item__shadow" ref="smaller" @scroll="sizeChange"> <div class="waterfall-item__holder--smaller"> </div> </div> <slot /> </div> </template> <script> mounted () { this.$nextTick(() => { this.$refs.bigger.scrollTop = '200000' this.$refs.smaller.scrollTop = '200000' }) }, methods: { sizeChange () { this.$parent.update(this) } } </script> <style> .waterfall-item { position: absolute; left: 0; right: 0; overflow: hidden; box-sizing: border-box; border: 1px solid black ; } .waterfall-item__shadow { height: 100%; left: 0; overflow: auto; position: absolute; top: 0; transform: translateX(200%); width: 100%; } .waterfall-item__holder--bigger { height: 200000px; } .waterfall-item__holder--smaller { height: 200%; } </style>
slot
爲用戶的真實 DOM,其撐開 waterfall-item
的高度。waterfall-item__shadow
與 waterfall-item
同高,從而使得用戶 DOM 的尺寸變化映射到 waterfall-item__shadow
上。waterfall-item__shadow
滾動到極限位置。waterfall-item__shadow
的 scroll
事件,在事件回調中通知 Waterfall.vue
組件更新對應子塊高度。// Waterfall.vue methods: { // ... update (child) { const index = this.$children.indexOf(child) this.childrenHeights.splice(index, 1, this.$children[index].$el.getBoundingClientRect().height) } }
在父組件中只須要更新此元素的高度便可,自會觸發後續佈局計算。
至此,可動態感知尺寸變化的萬能瀑布流也就完成了,使用現代瀏覽器點此預覽
預覽中定時修改部分 WaterfallItem 的字體大小,從而觸發子塊尺寸變化,觸發從新佈局。
在以上實現以外還能夠作一些其餘優化,如:
Waterfall.vue
添加的 add
和更新的 update
方法調用有重複(覆蓋)觸發的狀況,能夠合併。按需監聽尺寸變化,對 WaterfallItem 組件添加新的 props
,如:
scroll
監聽,且不渲染 waterfall-item__shadow
。once
,並在後續更新時再也不渲染 waterfall-item__shadow
。visibility: hidden
。activated
和 deactivated
中進行從新佈局的中止和激活,避免錯誤和沒必要要的開支。