前言
今天网上瞎逛,看到了一个瀑布流
实现的原理,瞅了两眼发现代码这么多,我心想这不是古董级
功能了吗。
于是,勾起了我一泡浓厚的兴趣, 准备把核心提炼
出来,顺带检验下我的技术这么多年有没有提升。
赶紧赶工,润色了一下,这里,言简意赅的把里面包含的小知识点给小伙伴们嚼碎了给大家。
正文开始
实际效果
瀑布流实现思路
-
确认需求:宽度固定,高度不固定的N条数据
-
获取数据,遍历,进行计算
-
得到实际宽高
-
根据实际宽高得到偏移量:
left、top
-
这一步,还可以做些额外的功能延伸(
懒加载
等)
-
-
进行渲染:把
计算好的
listData
赋给realDatas
渲染,避免页面闪动
整个瀑布流的计算核心围绕着如何得到DOM的实际宽高
, 然后根据Math.min.apply(null, this.colsHeightArr)
得到每一项的Top值
二种场景
一,纯图片
这种场景是网上几篇热门文章中的常客,我看到的一个写的很好的博主就是这种场景。
实现起来相对简单点,因为可以直接根据Img.onload
,然后根据计算去获取每个图片的实际宽高。
1.1, 得到实际宽高
js
let height;
this.listData.forEach((item, index) => {
let img = new Image();
img.src = item.src;
// 加载失败|加载成功都走这
img.onload = img.onerror = (e) => {
// 若加载失败则设置图片高度与宽度一致,加载成功则动态计算图片高度
height =
e.type === "load"
? Math.round(this.imgWidth * (img.height / img.width))
: this.imgWidth;
};
});
imgWidth
是根据 可用宽度/列数 - 间隙
得出的
1.2, 根据宽高得到偏移量:left、top
计算偏移量的方法其实和下面图文+首行DOM在未知区域展示
差不多,那里有核心点解释
。
js
let top;
let left;
let height;
if (this.beginIndex === 0) this.colsHeightArr = [];
this.listData.forEach((item, i) => {
let img = new Image();
img.src = item.src;
// 加载失败|加载成功都走这
img.onload = img.onerror = (e) => {
// 若加载失败则设置图片高度与宽度一致,加载成功则动态计算图片高度
height =
e.type === "load"
? Math.round(this.imgWidth * (img.height / img.width))
: this.imgWidth;
// 第一行
if (i < this.colNum) {
this.colsHeightArr.push(height);
top = 0;
left = i * this.colWidth;
} else {
// 找到最低的高度和其索引
const minHeight = Math.min.apply(null, this.colsHeightArr);
const minIdx = this.colsHeightArr.indexOf(minHeight);
top = minHeight;
left = minIdx * this.colWidth;
// 插入的盒子高度和最低盒子高度相加
this.colsHeightArr[minIdx] += height;
}
// 设置 img-box 位置
item.top = `${top}px`;
item.left = `${left}px`;
item.height = `${height}px`;
};
});
this.realDatas = [].concat(this.listData);
喝口水休息会
如果你是纯图片的瀑布流,那你不需要往下看啦~~~
二,图文+首行DOM在未知区域展示
2.1, 得到实际宽高
- 第一种方案(
推荐
):视图层添加一个带opacity属性的盒子,占位但是不展示,渲染后通过ref得到宽高
js
this.imgBoxEls = this.$refs.imgBox;
let height;
for (let i = this.beginIndex; i < this.imgBoxEls.length; ++i) {
if (!this.imgBoxEls[i]) return;
height = this.imgBoxEls[i].offsetHeight;
this.imgBoxEls[i].style.height = `${height}px`;
}
-
第二种方案:根据
createElement()
创建节点,填充数据,设置opacity,插入body,getBoundingClientRect()得到宽高
-
优点:直接操作DOM会少一次渲染,
速度快一点
-
缺点:费事,尤其是结构比较复杂的盒子。
-
js
const spans = document.createElement('div')
spans.style.opacity = '0'
spans.style.fontSize = '20px'
spans.style.fontFamily = 'Arial'
spans.innerText = '成熟市场是测试测试测试从'
document.body.appendChild(spans)
// 得到宽度
console.log(spans.getBoundingClientRect().width)
spans.remove()
2.2, 计算DOM偏移量
js
this.imgBoxEls = this.$refs["imgBox"];
if (!this.imgBoxEls) return;
let top, left, height;
if (this.beginIndex === 0) this.colsHeightArr = [];
for (let i = this.beginIndex; i < this.imgBoxEls.length; ++i) {
if (!this.imgBoxEls[i]) return;
height = this.imgBoxEls[i].offsetHeight;
// 第一行
if (i < this.colNum) {
this.colsHeightArr.push(height);
top = 0;
left = i * this.colWidth;
} else {
// 找到最低的高度和其索引
let minHeight = Math.min.apply(null, this.colsHeightArr);
let minIdx = this.colsHeightArr.indexOf(minHeight);
top = minHeight;
left = minIdx * this.colWidth;
this.colsHeightArr[minIdx] += height;
}
// 设置 img-box 位置
this.imgBoxEls[i].style.top = top + "px";
this.imgBoxEls[i].style.left = left + "px";
}
列数为2的时候
取对应DOM
top
值的原理
- 第1次遍历 colsHeightArr存入第1个DOM的高度
- 第2次遍历 colsHeightArr存入第2个DOM的高度
- 第3次遍历 colsHeightArr取最小值赋给第3个DOM的top,修改最小值:最小值=最小值+第3个DOM的高度
- 第4次遍历 colsHeightArr取最小值赋给第4个DOM的top,修改最小值:最小值=最小值+第4个DOM的高度
- 第5次遍历 colsHeightArr取最小值赋给第5个DOM的top,修改最小值:最小值=最小值+第5个DOM的高度
- ... 以此类推
这样就实现了每次遍历一个新DOM,都可以找到前2列里高度较小的DOM,然后插入进去
取对应DOM
left
值的原理
- 遍历时,
最小值的索引*列数2
这里,对已经得到的left,top值进行赋值也有两种方案
-
第一种方案(
推荐
):遍历中直接通过ref[i].style.top
直接添加DOM属性left、top
(直接操作DOM会少一次渲染,速度快一点
) -
第二种方案:遍历中
left、top
放进当前项
的属性中,视图层通过<div :style="{left: item.left,...}"/>
渲染
2.3, 计算父盒子高度:子盒子absolute,父盒子高度塌陷
传统的高度塌陷可以通过BFC
解决,但是很不幸,absolute
不在此列
这个问题会导致首行DOM在未知区域展示
时
- 父盒子设置了
relative
,因为没高度,子盒子展示不全,并且虽然有十几个子盒子,页面也是不能再滚动了。
- 父盒子未设置
relative
,子盒子页面置顶展示,遮挡已有页面
子盒子absolute
造成的父盒子高度塌陷,通常需要用js对子盒子高度进行计算,算出总值
此处,在遍历结束后,取colsHeightArr
最大值即可。
js
// 获取当前数据总高度给父盒子赋值
this.wfHeight = Math.max.apply(null, this.colsHeightArr);
2.4, 性能优化
提高性能的核心因素:beginIndex
见下文中的代码,逻辑是每次只对最新请求的数据做宽高+偏移量
计算,避免全量计算造成性能浪费。
三,完整代码
js
<div
class="column-index waterfall"
:style="{ height: wfHeight + 'px', opacity: wfLoad ? '1' : '0' }"
>
<div
v-for="item in realDatas"
ref="imgBox"
:key="item.resourceId"
class="column-item p-6 inline-block"
:style="{ width: imgWidth + 'px' }"
@click="xxx"
>
... doSth
</div>
</div>
export default {
data() {
return {
wfLoad: false,
listData: [],
realDatas: [], // 渲染的图片
imgCol: 2, // 图片列数
imgGap: 1, // 图片间间隔
loadedCount: 0, // 记录加载完毕的数量
imgBoxEls: [], // 所有 img-box 元素
beginIndex: 0,
colsHeightArr: [], // 保存当前每一列的高度
wfHeight: 0,
}
},
computed: {
// 容器 waterfall 的宽度
waterfallWidth() {
console.log(this.$refs.waterfall);
return this.$refs.waterfall.clientWidth;
},
// 图片宽度
imgWidth() {
return this.colWidth - 2 * this.imgGap;
},
// 列宽度
colWidth() {
return this.waterfallWidth / this.colNum;
},
// 列数
colNum() {
return this.imgCol;
},
},
watch: {
listData() {
this.preloaded();
},
},
created() {
this.load();
},
methods:{
load(){
// 接口获取数据
this.listData = xxx
},
preloaded() {
// 中介数组,配合$nextTick缓冲触底获取新数据后的渲染
this.realDatas = [].concat(this.listData);
// 当加载完以后 页面开始进行渲染 realDatas 为真实渲染数组
this.$nextTick(() => {
this.waterfall();
});
},
waterfall() {
// 等到整个视图都渲染完毕再执行
this.imgBoxEls = this.$refs.imgBox;
if (!this.imgBoxEls) return;
let top;
let left;
let height;
if (this.beginIndex === 0) this.colsHeightArr = [];
// 提高性能的核心因素:beginIndex
for (let i = this.beginIndex; i < this.imgBoxEls.length; ++i) {
if (!this.imgBoxEls[i]) return;
height = this.imgBoxEls[i].offsetHeight;
// 第一行
if (i < this.colNum) {
this.colsHeightArr.push(height);
top = 0;
left = i * this.colWidth;
} else {
// 找到最低的高度和其索引
const minHeight = Math.min.apply(null, this.colsHeightArr);
const minIdx = this.colsHeightArr.indexOf(minHeight);
top = minHeight;
left = minIdx * this.colWidth;
// 插入的盒子高度和最低盒子高度相加
this.colsHeightArr[minIdx] += height;
}
// 设置 img-box 位置
this.imgBoxEls[i].style.top = `${top}px`;
this.imgBoxEls[i].style.left = `${left}px`;
this.imgBoxEls[i].style.height = `${height}px`;
// 当前图片在窗口内,则加载
// doSth...
}
// 获取当前数据总高度给父盒子赋值
this.wfHeight = Math.max.apply(null, this.colsHeightArr);
this.beginIndex = this.imgBoxEls.length;
this.wfLoad = true;
},
}
}
完结
这篇文章我尽力把我的笔记和想法放到这了,希望对小伙伴有帮助。
欢迎转载,但请注明来源。
最后,希望小伙伴们给我个免费的点赞,祝大家心想事成,平安喜乐。