使用 Vue 從零開始手寫一個貓咪瀑布流組件
點擊上方?前端Q,關(guān)注公眾號
回復(fù)加群,加入前端Q技術(shù)交流群
貓咪瀑布流
如下動態(tài)圖,一張張不規(guī)則的可愛貓咪照片是否勾起了你的少女心呢?
瀑布流又稱瀑布流式布局,是比較流行的一種網(wǎng)站頁面布局方式。瀑布流實現(xiàn)的方式有很多種,但是原理都是差不多的,本文我們來詳細介紹下下面這個貓咪瀑布流是如何實現(xiàn)的。
瀑布流原理

如上圖:第1、2、3、4、5張圖排在容器內(nèi)的第一行,即靠近頂部。
我們會發(fā)現(xiàn)第6張圖并沒有排在第1張圖的下面,而是排在了第3張圖下面。
其實這就是瀑布流的關(guān)鍵之處,那么第6張圖片是根據(jù)什么排列的呢?
其實他會放在當前排列圖片中底部距離頂部最小的圖片下面,這樣做是為了圖片差不會很大,我們可以看到3是高度最小的圖片,然后我們就將第6張圖放在3圖的下面。
那么同理,第7張圖就應(yīng)該在下圖所示位置。

那么你知道第8張圖應(yīng)該放在哪里嗎,這里我們留個問題讓大家思考,文章結(jié)尾我們會揭曉答案,你要是迫不及待可以滑到文章結(jié)尾看看你猜的對不對。
預(yù)加載圖片
實現(xiàn)瀑布流的原理我們大概知道,那么具體的技術(shù)實現(xiàn)是怎么實現(xiàn)呢?
其實就是根據(jù)圖片寬高等設(shè)置圖片的偏移值即top和left值。
意味著我們肯定需要知道圖片的寬高比例,因為我們這里的一列的寬度需要保持一致,即可以設(shè)置一個固定值。
如果我們等渲染完以后再進行高度的獲取,然后再設(shè)置top值和left值,就會導(dǎo)致界面的閃動。
所以我們需要再一開始就先預(yù)加載圖片并獲取寬高,但是并不進行渲染等時機成熟,也就是所有圖片都加載完成,即所有圖片的高度都算出來以后再進行渲染,說起來柑橘很簡單,但是具體實現(xiàn)應(yīng)該怎么操作呢?
1.遍歷傳進來的img數(shù)組
//imgsArr是組件外部傳入的一個圖片數(shù)組?里面有一個src表示圖片的路徑
this.imgsArr.forEach((imgItem,?imgIndex)?=>?{
????//...
????//...
})
復(fù)制代碼
2.loadedCount記錄加載數(shù)量
//聲明loadedCount變量記錄加載完畢的數(shù)量,為了和imgsArr大小作比較,通知加載完畢(包括無圖、加載完畢,加載失敗的情況)
data(){
????return?{
????????loadedCount:?0
????}
}
復(fù)制代碼
3.無圖的情況下
//?無圖時?將高度記錄為0
if?(!imgItem[this.srcKey])?{
????this.imgsArr[imgIndex]._height?=?"0";
????this.loadedCount++;
????//?支持無圖模式
????if?(this.loadedCount?==?this.imgsArr.length)?{
????????this.$emit("preloaded");
????}
????return;
}
復(fù)制代碼
4.Image對象
//使用Image?API?當src屬性改變并完成加載時執(zhí)行
let?oImg?=?new?Image();
oImg.src?=?imgItem[this.srcKey];
oImg.onload?=?oImg.onerror?=?(e)?=>?{
????//...
}
復(fù)制代碼
5.加載完成后,計算實際需要渲染圖片的高
//理論上?預(yù)加載圖片的高度/預(yù)加載圖片的寬度=需要渲染圖片的高度/圖片寬度
this.imgsArr[imgIndex]._height?=
????????????e.type?==?"load"
????????????????Math.round(this.imgWidth_c?*?(oImg.height?/?oImg.width))
??????????????:?this.imgWidth_c;??
復(fù)制代碼
6.加載失敗后,標識失敗標記
if?(e.type?==?"error")?{
????this.imgsArr[imgIndex]._error?=?true;
????this.$emit("imgError",?this.imgsArr[imgIndex]);
}?
復(fù)制代碼
7.全部加載完后,進行emit preloaded事件
if?(this.loadedCount?===?this.imgsArr.length)?{?
????this.$emit("preloaded");
}
復(fù)制代碼
計算列數(shù)

calcuCols()?{
????//?需要計算出渲染多少列數(shù)據(jù)
????let?waterfallWidth?=?this.width???this.width?:?window.innerWidth;
????//最少渲染一列
????let?cols?=?Math.max(parseInt(waterfallWidth?/?this.colWidth),1);?
????//最大不能超過maxCols列
????return?this.isMobile???2?:?Math.min(cols,this.maxCols;
}
復(fù)制代碼
使用on/on/on/emit監(jiān)聽加載完畢

//當加載完以后?頁面開始進行渲染?imgsArr_c?為真實渲染數(shù)組
this.$emit("preloaded");
this.$on("preloaded",?()?=>?{?
????this.imgsArr_c?=?this.imgsArr.concat([]);?//?預(yù)加載完成,這時才開始渲染
????//?...
});
復(fù)制代碼
使用$nextTick尋找更新時機

當data中的某個屬性改變的時候,這個值并不是立即渲染到頁面上,而是先放到watcher隊列上(異步),只有當前任務(wù)空閑的時候才會去執(zhí)行watcher隊列上的任務(wù)。所以導(dǎo)致,改變的數(shù)據(jù)掛載到dom上會有一定的延遲,這也就導(dǎo)致了,當我們在改變屬性值的時候,立即通過dom去拿改變的值時發(fā)現(xiàn)拿到的值并不是改變的值,而是之前的值。
上面的data也就是對應(yīng)了我們的imgsArr_c。
this.$nextTick作用:在下次dom更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)。在修改數(shù)據(jù)之后立即使用這個方法,獲得更新后的dom。
this.$nextTick(()?=>?{
????//表示欲加載結(jié)束
????this.isPreloading?=?false;
????this.waterfall();
});
復(fù)制代碼
使用waterfall方法排列(核心)

waterfall()?{
????//選擇所有圖片
????this.imgBoxEls?=?this.$el.getElementsByClassName("img-box");
????//如果一個都沒有則沒有東西可以排列?故直接返回
????if?(!this.imgBoxEls)?return;
????//聲明top、left、height、colwidth即列的寬度
????let?top,
????????left,
????????height,
????????colWidth?=?this.isMobile
????????????this.imgBoxEls[0].offsetWidth
??????????:?this.colWidth;
????//開始排列的坐標大小?如果是從0開始排列?則將colsHeightArr置空,colsHeightArr的作用是用來比較?當前排列圖片中哪個最小
????if?(this.beginIndex?==?0)?this.colsHeightArr?=?[];
????//從0開始排列
????for?(let?i?=?this.beginIndex;?i?this.imgsArr.length;?i++)?{
????????if?(!this.imgBoxEls[i])?return;
????????//獲取渲染元素的高度
????????height?=?this.imgBoxEls[i].offsetHeight;
????????if?(i?this.cols)?{
????????????//如果小于列數(shù)?則將第一排的幾個元素全部push進數(shù)組里面?將top置為0?left為列坐標乘以列的寬度
????????????this.colsHeightArr.push(height);
????????????top?=?0;
????????????left?=?i?*?colWidth;
????????}?else?{
????????????//當?shù)谝恍信帕型暌院?算出當前最小的高度
????????????let?minHeight?=?Math.min.apply(null,?this.colsHeightArr);?//?最低高低
????????????//當?shù)谝恍信帕型暌院?算出當前最小的索引
????????????let?minIndex?=?this.colsHeightArr.indexOf(minHeight);?//?最低高度的索
????????????//新元素的top值即為數(shù)組中最小的值
????????????top?=?minHeight;
????????????//左邊的值即為最小索引乘以列寬
????????????left?=?minIndex?*?colWidth;
????????????//?設(shè)置元素定位的位置
????????????//?更新colsHeightArr
????????????this.colsHeightArr[minIndex]?=?minHeight?+?height;
????????}
????????//設(shè)置單個元素的left、top值
????????this.imgBoxEls[i].style.left?=?left?+?"px";
????????this.imgBoxEls[i].style.top?=?top?+?"px";
??????}
??????this.beginIndex?=?this.imgsArr.length;?//?排列完之后,新增圖片從這個索引開始預(yù)加載圖片和排列
}
復(fù)制代碼
添加響應(yīng)式

window.addEventListener("resize",?this.response);
response:?function?()?{
??????let?old?=?this.cols;
??????//重新計算列數(shù)
??????this.cols?=?this.calcuCols();
??????//如果列數(shù)不變?則不需要重新排列
??????if?(old?===?this.cols)?return;?//?列數(shù)不變直接退出
??????this.beginIndex?=?0;?//?開始排列的元素索引
??????this.waterfall();
}
復(fù)制代碼
添加滾動觸底

this.scroll();
scroll()?{
??????this.$refs.scrollEl.addEventListener("scroll",?this.scrollFn);
}
scrollFn()?{
??????let?scrollEl?=?this.$refs.scrollEl;
??????//如果正在預(yù)加載
??????if?(this.isPreloading)?return;
??????let?minHeight?=?Math.min.apply(null,?this.colsHeightArr);
??????if?(
????????scrollEl.scrollTop?+?scrollEl.offsetHeight?>
????????minHeight?-?this.reachBottomDistance
??????)?{
????????this.isPreloading?=?true;
????????this.$emit("scrollReachBottom");
??????}
}
復(fù)制代碼
更多細節(jié)
更多細節(jié),源碼盡在github[2]上,歡迎大家踴躍star!
發(fā)布到npm上供大家使用
npm?install?@parrotjs/vue-waterfall?-S
復(fù)制代碼
具體可以去我的github README.md進行查看

關(guān)于本文
作者:安穩(wěn)
https://juejin.cn/post/7026253551361851405

往期推薦



最后
歡迎加我微信,拉你進技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認真學(xué)前端,做個專業(yè)的技術(shù)人...


