使用 Vue 從零開(kāi)始手寫一個(gè)貓咪瀑布流組件
貓咪瀑布流
如下動(dòng)態(tài)圖,一張張不規(guī)則的可愛(ài)貓咪照片是否勾起了你的少女心呢?
瀑布流又稱瀑布流式布局,是比較流行的一種網(wǎng)站頁(yè)面布局方式。瀑布流實(shí)現(xiàn)的方式有很多種,但是原理都是差不多的,本文我們來(lái)詳細(xì)介紹下下面這個(gè)貓咪瀑布流是如何實(shí)現(xiàn)的。

瀑布流原理

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

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

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

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

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

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

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

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ù)制代碼
更多細(xì)節(jié)
更多細(xì)節(jié),源碼盡在github[2]上,歡迎大家踴躍star!
發(fā)布到npm上供大家使用
npm?install?@parrotjs/vue-waterfall?-S
復(fù)制代碼
具體可以去我的github README.md進(jìn)行查看

關(guān)于本文
作者:安穩(wěn)
https://juejin.cn/post/7026253551361851405
點(diǎn)贊和在看就是最大的支持??
