# waterfall **Repository Path**: shuinger/waterfall ## Basic Information - **Project Name**: waterfall - **Description**: 瀑布流demo - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2022-09-19 - **Last Updated**: 2022-09-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ​ 最近看到一篇使用[vue从零开始手写一个猫咪瀑布流组件(支持ssr)](https://juejin.cn/post/7026253551361851405),讲了关于如何用 Vue 来实现瀑布流,学习后自己动手写了一个简单的 demo。 瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式,每张图片的宽度都设置为一样,但是高度是根据内容变化的,实现一个不规则的排列。 [demo展示](https://img-blog.csdnimg.cn/551fd396facc4673871c4af041267cfa.gif) ### 原理 原理其实很简单,简单来说就是把每张图都设置为绝对定位,再根据宽高设置图片的偏移值,则 left 和 top 属性确定位置。 ![输入图片说明](https://images.gitee.com/uploads/images/2021/1122/112916_6000d13a_7588660.png "屏幕截图.png") 如上图,可以看到,前五张图排在第一行(demo 设置 PC 浏览器显示 5 列,移动端浏览器显示 2 列)。 接下来的第六张图应该放在什么位置?应该放在红色框这个位置而不是黑色框这个位置。 这就是瀑布流最关键的地方,除了第一行是按顺序排列之外,其他的图片应该是按照 放到当前高度最小的列下面(可以理解为见缝插针?hhh)。所以第六张图排列后如下图所示,同理之后的每一张图片都按这个规则排列: ![输入图片说明](https://images.gitee.com/uploads/images/2021/1122/112928_eb14b600_7588660.png "屏幕截图.png") ### 图片预处理 通过上图可以发现,要实现这种效果,每个图片的宽度都是一样的(应该也可以实现宽度不同时的效果,这应该还需要再考虑当前的空隙过小无法插入图片的情况,后期再尝试),即每一列的宽度一致(在 demo 我是用当前窗口的宽度 / 列数 得到每一列的宽度)。 然后再通过图片的宽度来设定图片的高度,其实就是把图片按原始比例缩放。 ```javascript /** 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 ```html
``` ```javascript preloaded() { // 开始渲染 this.imgsArr_c = [].concat(this.imgsArr); .... } ``` 渲染完之后,接下来就是对每张图进行排列。 这里需要注意的是,vue 中当 data 改变后,并不是立即渲染到页面上的,而是先放到 watcher 队列上,只有当任务空闲时才会执行 watcher 队列上的任务。这就导致了,数据改变后挂载在 dom 上可能会存在一定的延迟,所以数据改变后立刻去获取 dom 元素可能拿到的不是最新的值而是改变前的值。 所以,使用 $nextTick 来对解决这个问题,$nextTick 的作用就是在下次 dom 更新循环结束后执行其 callback。在修改 imgsArr_c 之后使用这个方法,才能保证排列的元素是更新后的。 ```javascript preloaded() { // 开始渲染 this.imgsArr_c = [].concat(this.imgsArr); this.$nextTick(() => {. // 对每个元素进行排列 this.waterfall(); }); } ``` ### 设置瀑布流(核心) ```javascript // 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 时间,并使用节流进行了优化。 ```javascript // 节流函数 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); ``` ```javascript // 滚动触底事件 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 函数。 ```javascript watch: { imgsArr(newVal, oldVal) { this.preLoad(); } } ``` ### 图片懒加载 除了复现了原博主的功能,我在此加入了图片懒加载的功能。 图片懒加载:网页上的所有图片不是默认加载的,而是等到该图片出现在浏览器的可视区域后才加载。懒加载是一种优化网页性能的方式。 懒加载原理: 图片要加载,即浏览器发出请求,是根据 img 标签是否有 src 属性来决定的,所以实现懒加载就要在 src 属性入手,当图片没进入可视区域时,不给 img 标签设置 src 属性,等到图片进入可是区域是再设置,这时图片才会加载。 ```javascript // 滚动触底事件 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 属性。 ​