⚡年少时被瀑布流毒打过?现在来毒打瀑布流吧

前言

今天网上瞎逛,看到了一个瀑布流实现的原理,瞅了两眼发现代码这么多,我心想这不是古董级功能了吗。

于是,勾起了我一泡浓厚的兴趣, 准备把核心提炼出来,顺带检验下我的技术这么多年有没有提升。

赶紧赶工,润色了一下,这里,言简意赅的把里面包含的小知识点给小伙伴们嚼碎了给大家。

正文开始

实际效果

瀑布流实现思路

  • 确认需求:宽度固定,高度不固定的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;
      },
    }
}

完结

这篇文章我尽力把我的笔记和想法放到这了,希望对小伙伴有帮助。

欢迎转载,但请注明来源。

最后,希望小伙伴们给我个免费的点赞,祝大家心想事成,平安喜乐。

相关推荐
星就前端叭44 分钟前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
m0_748234521 小时前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
Web阿成1 小时前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript
噢,我明白了1 小时前
同源策略:为什么XMLHttpRequest不能跨域请求资源?
javascript·跨域
sanguine__1 小时前
APIs-day2
javascript·css·css3
苹果醋32 小时前
Golang的文件加密工具
运维·vue.js·spring boot·nginx·课程设计
jwensh2 小时前
【Jenkins】Declarative和Scripted两种脚本模式有什么具体的区别
运维·前端·jenkins
关你西红柿子2 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
益达是我2 小时前
【Chrome】浏览器提示警告Chrome is moving towards a new experience
前端·chrome
济南小草根2 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js