用 vue 实现瀑布流
最近看到一篇好文章,讲了关于如何用 Vue 来实现瀑布流,学习后自己动手写了一个简单的 demo。瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。类似小红书主页一样,每张图片的宽度都设置为一样,但是高度是根据内容变化的,实现一个不规则的排列。原理原理其实很简单,简单来说就是把每张图都设置为绝对定位,再根据宽高设置图片的偏移值,则 left 和 top 属性确定位置。如上图,可以看到,前五
最近看到一篇使用vue从零开始手写一个猫咪瀑布流组件(支持ssr),讲了关于如何用 Vue 来实现瀑布流,学习后自己动手写了一个简单的 demo。
瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式,每张图片的宽度都设置为一样,但是高度是根据内容变化的,实现一个不规则的排列。
原理
原理其实很简单,简单来说就是把每张图都设置为绝对定位,再根据宽高设置图片的偏移值,则 left 和 top 属性确定位置。
如上图,可以看到,前五张图排在第一行(demo 设置 PC 浏览器显示 5 列,移动端浏览器显示 2 列)。
接下来的第六张图应该放在什么位置?应该放在红色框这个位置而不是黑色框这个位置。
这就是瀑布流最关键的地方,除了第一行是按顺序排列之外,其他的图片应该是按照 放到当前高度最小的列下面(可以理解为见缝插针?hhh)。所以第六张图排列后如下图所示,同理之后的每一张图片都按这个规则排列:
图片预处理
通过上图可以发现,要实现这种效果,每个图片的宽度都是一样的(应该也可以实现宽度不同时的效果,这应该还需要再考虑当前的空隙过小无法插入图片的情况,后期再尝试),即每一列的宽度一致(在 demo 我是用当前窗口的宽度 / 列数 得到每一列的宽度)。
然后再通过图片的宽度来设定图片的高度,其实就是把图片按原始比例缩放。
/**
imgsArr 为图片数组
loadedCount 为已预处理好的图个数
imgWidth 为图片的宽度
*/
// 预加载 设置并保存图片宽高
preLoad() {
// forEach 无法通过 item 直接修改数组元素,需用数组下标修改
this.imgsArr.forEach((item, index) => {
if (index < this.loadedCount)
return;
// 无图则把高度设置为0
if (!item.src) {
// 图片的高度
this.imgsArr[index]._height = "0";
++this.loadedCount;
// 当前图片都与处理完,则加载图片
if (this.imgsArr.length === this.loadedCount) {
this.preloaded();
}
} else {
let img = new Image();
img.src = item.src;
img.onload = img.onerror = (e) => {
// 若加载失败则设置图片高度与宽度一致,加载成功则动态计算图片高度
this.imgsArr[index]._height = e.type === "load" ? Math.round(this.imgWidth * (img.height / img.width)) : this.imgWidth
if (e.type === "error") {
this.imgsArr[index]._error = true;
}
++this.loadedCount;
// 当前图片都与处理完,则加载图片
if (this.imgsArr.length === this.loadedCount) {
this.preloaded();
}
}
}
})
},
加载图片
设置完每张图的高度后,再开始进行渲染,把 imgsArr_c 为真实的渲染数组。
使用一个 div 包含 img 标签,在 div 标签上动态设置宽高,宽则对应图片的宽度 imgWidth(也就是上面所提及的列宽),高则对应着在预处理阶段为每张图片所设置的高度 item._height
<div class="img-box default-card-animation" v-for="(item, index) in imgsArr_c" :key="index"
:style="{width: imgWidth + 'px', height: item._height + 'px'}" ref="imgBox">
<img :data-src="item.src" />
</div>
preloaded() {
// 开始渲染
this.imgsArr_c = [].concat(this.imgsArr);
....
}
渲染完之后,接下来就是对每张图进行排列。
这里需要注意的是,vue 中数据的变化到 DOM
的重新渲染是一个异步过程。vue 中当 data 改变后,并不是立即渲染到页面上的,而是先放到事件队列上。然后,在下一个的事件循环中,Vue
刷新事件队列并执行实际 (已去重的) 工作。这就导致了,数据改变后挂载在 dom 上可能会存在一定的延迟,所以数据改变后立刻去获取 dom 元素可能拿到的不是最新的值而是改变前的值。
所以,使用 $nextTick 来对解决这个问题,$nextTick 的作用就是在下次 dom 更新循环结束后执行其 callback。在修改 imgsArr_c 之后使用这个方法,才能保证排列的元素是更新后的。
preloaded() {
// 开始渲染
this.imgsArr_c = [].concat(this.imgsArr);
this.$nextTick(() => {.
// 对每个元素进行排列
this.waterfall();
});
}
设置瀑布流(核心)
// waterfall,等到整个视图都渲染完毕再执行
waterfall() {
// 选择所有图片
this.imgBoxEls = this.$refs["imgBox"];
// 若没图片,则返回
if (!this.imgBoxEls)
return;
let top, left, height;
// 开始排列的坐标,若为0则重头排列,colsHeightArr 数组保存的是当前每一列的高度
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;
// colWidth 为列宽,等于图片宽度加 div 左右的padding,colWidth = imgWdith + 2 * padding
left = i * this.colWidth;
} else {
// 找到当前最低的高度和其索引
let minHeight = Math.min.apply(null, this.colsHeightArr);
let minIdx = this.colsHeightArr.indexOf(minHeight);
// 当前图片的 top,即当前图片应所在的高度
top = minHeight;
// 当前图片的 left,即当前图片应该排到目前高度最低那一列下面
left = minIdx * this.colWidth;
// 更新第 minIdx 列的高度
this.colsHeightArr[minIdx] += height;
}
// 设置 img-box 位置
this.imgBoxEls[i].style.top = top + "px";
this.imgBoxEls[i].style.left = left + "px";
// 当前图片在窗口内,则加载,这是用于后面的图片懒加载。viewHeight 为窗口高度
if (top < this.viewHeight) {
let imgEl = this.imgBoxEls[i].children[0];
imgEl.src = imgEl.getAttribute("data-src");
imgEl.style.opacity = 1;
imgEl.style.transform = "scale(1)";
}
}
// 排列完之后,之后新增图片从这个索引开始预加载图片和排列,之前排列的图片无需在处理
this.beginIndex = this.imgBoxEls.length;
}
触底更新图片(这个可以根据自己的需求修改)
demo 中实现了触底则在 imgsArr 中添加图片后,自动渲染并排列。
这里为浏览器的 scroll 时间绑定了 scrollFn 时间,并使用节流进行了优化。
// 节流函数
throttle(fn, time) {
let canRun = true;
return function () {
if (!canRun)
return;
canRun = false;
setTimeout(() => {
fn.apply(this);
canRun = true;
}, time)
}
}
window.onscroll = this.throttle(this.scrollFn, 500);
// 滚动触底事件
scrollFn() {
let minHeight = Math.min.apply(null, this.colsHeightArr);
// 滚动条滚动的高度
let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
// 到达最底层的高度最低的一列
if (scrollTop + this.viewHeight > minHeight - this.reachBottomDistance){
// 修改 imgsArr 数组
this.imgsArr = this.imgsArr.concat(this.imgsArr);
}
....
}
到达底部后,为 imgsArr 数组添加新图片,要让其自动渲染并排列,在这使用了 watch 来对 imgsArr 进行监听。当 imgsArr 被修改时,调用 preLoad 函数。
watch: {
imgsArr(newVal, oldVal) {
this.preLoad();
}
}
图片懒加载
除了复现了原博主的功能,我在此加入了图片懒加载的功能。
图片懒加载:网页上的所有图片不是默认加载的,而是等到该图片出现在浏览器的可视区域后才加载。懒加载是一种优化网页性能的方式。
懒加载原理:
图片要加载,即浏览器发出请求,是根据 img 标签是否有 src 属性来决定的,所以实现懒加载就要在 src 属性入手,当图片没进入可视区域时,不给 img 标签设置 src 属性,等到图片进入可是区域是再设置,这时图片才会加载。
// 滚动触底事件
scrollFn() {
let minHeight = Math.min.apply(null, this.colsHeightArr);
// 滚动条滚动的高度
let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
....
// 图片懒加载
this.imgBoxEls.forEach((imgBoxEl, index) => {
let imgEl = imgBoxEl.children[0];
// 若已加载,则跳过
if (imgEl.src)
return;
// 当前图片所处的高度
let top = imgBoxEl.style.top;
top = Number.parseFloat(top.slice(0, top.length - 2));
// 图片已到达可视范围,则加载
if (scrollTop + this.viewHeight > top) {
imgEl.src = imgEl.getAttribute("data-src")
imgEl.style.opacity = 1;
imgEl.style.transform = "scale(1)";
}
})
}
在滚动事件内,遍历 imgBoxEls 数组(页面上的每一个图片元素),通过判断当前滚动条滑动的高度 scrollTop + 浏览器可视高度 viewHeight 是否大于图片所在页面的高度 top(在 waterfall 函数中设置的 top 属性)。当大于,表明该图片已进入可视区域,则设置 src 属性。
更多推荐
所有评论(0)