前言
- 常网IT源码上线啦!
 - 本篇录入技术选型专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
 - 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
 - 接下来想分享一些自己在项目中遇到的技术选型以及问题场景。
 
我们的经济是一片大海,而不是一个小池塘,大海啊,有风平浪静之时,也有疯狂暴雨之时,没有疯狂暴雨,那就不是大海了。狂风骤雨可以掀翻小。但不能掀翻大海。经历了无数次狂风骤雨,大海依旧在那儿,经历了5000多年的艰难困苦,依旧在那儿。面向未来,将永远在这儿。

一、前言
页面中显示四列卡片,每一个卡片的高度根据内容决定(即:高度不相同),宽度相同。
期望效果:如同小红书的瀑布流形式排列。
元素以不规则的方式排列,就像瀑布中的流水一样,每个元素的高度可以不同。
二、分析
现在使用的技术栈是flex布局,正如图中所示,第一个卡片的内容较少,第二个最长。

第五个为第二行,现在被排列的位置,期望排列在第一个的后面。
如图所示:(期望效果)

三、相对re+绝对ab
我现在的做法:相对re+绝对ab
因为我是一行四列,宽度设置了23%。用手动定位的形式,控制left和top设置距离位置。
            
            
              java
              
              
            
          
          <div class="body">
  <div class="body-item" v-for="(card, cardIndex) in cardList"  
      :style="{ left: left(cardIndex) }"></div>
</div>
<style scoped lang='scss'>
.body {
  width: 100%;
  // 本来用的flex布局,但会出现图一的情况
  //   display: flex;
  //   flex-wrap: wrap;
  gap: 30px;
  position: relative;
  // height: 3000px; 不能写死
  &-item {
    width: 23%;
    position: absolute;
  }
}
</style
        设置每个元素的left,可以用计算属性
解释:
第一个元素的left:0
第二个元素:23%(元素本身的宽度) + 2%(这是元素与元素之前的边距)
第三个元素:23% + 2% + 23% + 2% = 50%

第四个:23% + 2% + 23% + 2% + 23% + 1% = 74%
baseValues这个就是我们的边距
groupOffset:前面元素的宽度
            
            
              java
              
              
            
          
          computed: {
  left() {
      return (index) => {
        const groupOffset = (index % 4) * 23
        const baseValues = [1, 2, 4, 5]
        const base = groupOffset ? baseValues[index % 4] : 0 // 循环取基础值
        return index ? `${groupOffset + base}%` : 0
      }
    },
}
        效果

left搞定,剩下top
top本来也是想和top绑定在元素上面,动态属性计算,但会动态计算出元素的高度,发现不实时。

offsetHeight:取的值不是很准确
而且我还要读取上一次元素的高度,所以就不在计算属性这里写了。
            
            
              java
              
              
            
          
          top(){
  this.$nextTick(() => {
    return (index) => {
      const item = document.querySelectorAll('.Visualization-item')
      return item[index].offsetHeight
    }
  }
}
        解释:
第一行的top我们都设为0
group :Math.floor(index / 4):用于计算元素所在的行号,知道行号,就知道取第几行的元素了
            
            
              java
              
              
            
          
          // 假设元素索引为:
// 第0行:0,1,2,3
// 第1行:4,5,6,7
// 第2行:8,9,10,11...
Math.floor(0/4) = 0   // 第0行
Math.floor(5/4) = 1   // 第1行
Math.floor(8/4) = 2   // 第2行
        循环内,主要是计算:假设要计算第6个元素,那top等于 =
- 
我要先找他是第几行,(第2行)
 - 
第1行的第2个元素的高度
 
如果第10个,top等于
- 
他是第3行
 - 
第1行的第2个元素的高度 + 第2行的第2个元素(6)的高度
 
所以可以看到,我们会用到累加高度
            
            
              java
              
              
            
          
          mounted() {
  // 循环累加前序行中同列元素的高度和间隔
  // 最终高度 = 所有前序行同列元素高度之和 + 间隔总和
  document.querySelectorAll('.Visualization-item').forEach((f, index) => {
      this.$nextTick(() => {
        //    0 1 2 3
        //    4 5 6 7
        //    8 9 10 
        const baseIndex = index % 4 // 基础索引 0-3
        const group = Math.floor(index / 4) // 行号
        // 计算累计高度:前序行高度之和 + 间隔
        let totalHeight = 0
        for (let i = 0; i < group; i++) {
          const prevIndex = baseIndex + i * 4
          totalHeight += this.getFirstItemHeight(prevIndex) + 20
        }
        f.style.top = `${totalHeight}px`
      })
    })
}
        我们用定位相对re+绝对ab,就要给父元素设置高度,高度要自动计算
            
            
              java
              
              
            
          
          methods: {
  setEleHeight() {
    const container = document.querySelector('.Visualization');
    let maxBottom = 0;
    document.querySelectorAll('.Visualization-item').forEach((f, index) => {
      this.$nextTick(() => {
        // ... 原有定位逻辑 ...
        f.style.top = `${totalHeight}px`
        // 新增高度计算
        const itemBottom = totalHeight + f.offsetHeight;
        if (itemBottom > maxBottom) {
          maxBottom = itemBottom;
        }
        container.style.height = `${maxBottom + 50}px`; // 增加50px缓冲
      })
    })
  }
}
        样式部分需要移除固定高度:
            
            
              java
              
              
            
          
          .Visualization {
  // 注释掉固定高度
  // height: 3000px;
}
        完美了

四、优化
你可是高级前端开发,我这里的瀑布流用的是动态设置left和top,会不会影响性能呢?第一反应要想到这,改成transform是不是比较好?
那肯定。
其实我是参考小红书的,平时娱乐的时候也要思考技术栈

            
            
              java
              
              
            
          
          // 传统定位 (触发回流)
element.style.left = '100px'; 
// transform 定位 (只触发重绘)
element.style.transform = 'translateX(100px)';
        - 
GPU 加速渲染
 - 
60fps 流畅动画
 - 
最小的回流触发
 
当元素需要频繁更新位置时,这种实现方式比直接操作 left/top 属性性能提升约 30%-50%。
八股文背归背,要实际用上。
开搞。
            
            
              java
              
              
            
          
          <div class="Visualization">
  <div class="Visualization-item" v-for="(card, cardIndex) in cardList"></div>
</div>
<style scoped lang='scss'>
.Visualization {
  width: 100%;
  // 本来用的flex布局,但会出现图一的情况
  //   display: flex;
  //   flex-wrap: wrap;
  gap: 30px;
  position: relative;
  &-item {
    width: 23%;
    position: absolute;
    left: 0;
    top: 0;
    // 添加GPU加速
  will-change: transform;
  // 优化过渡效果
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);  /* 浏览器会使用独立的合成层渲染 */
  }
}
</style
        js处理
transform:translate的translateX就是我们的left,y轴就是我们的top
- 
而top还是之前的逻辑
 - 
left不能写百分比了,我们可以巧妙的将百分比转换为精确像素值(父元素的宽度 * 百分比) / 100即可
 
生怕大家看不太懂,所以注释加得比较多~
            
            
              java
              
              
            
          
          setEleHeight() {
      const container = document.querySelector('.Visualization')
      let maxBottom = 0
      document.querySelectorAll('.Visualization-item').forEach((f, index) => {
        this.$nextTick(() => {
          const baseIndex = index % 4 // 基础索引 0-3
          const group = Math.floor(index / 4) // 行号
          // 计算累计高度:前序行高度之和 + 间隔
          let totalHeight = 0
          for (let i = 0; i < group; i++) {
            const prevIndex = baseIndex + i * 4
            totalHeight += this.getFirstItemHeight(prevIndex) + 20
          }
          // 计算横向偏移(将百分比转换为像素)
          const containerWidth = f.parentElement.offsetWidth // 父容器宽度
          const groupOffset = (index % 4) * 23 // 百分比偏移
          const baseValues = [1, 2, 4, 5]
          const base = groupOffset ? baseValues[index % 4] : 0
          const translateX = (containerWidth * (groupOffset + base)) / 100 // 将百分比转换为精确像素值
          //   f.style.top = `${totalHeight}px`
          f.style.transform = `translate(${translateX}px, ${totalHeight}px)`
          // 新增高度计算
          const itemBottom = totalHeight + f.offsetHeight // 之前的高度 + 自己的高度
          if (itemBottom > maxBottom) {
            maxBottom = itemBottom
          }
          container.style.height = `${maxBottom + 50}px` // 增加50px缓冲
        })
      })
    },
        效果

那我们的left是px,如果窗口大小改变,要重新计算一下,还要考虑防抖
            
            
              java
              
              
            
          
          methods:{
  // 新增防抖函数
    debounce(fn, delay) {
      let timeoutId
      return (...args) => {
        clearTimeout(timeoutId)
        timeoutId = setTimeout(() => fn.apply(this, args), delay)
      }
    },
    handleResize() {
      this.debounce(this.setEleHeight, 200)()
    },
},
mounted() {
    this.setEleHeight()
    window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
    window.removeEventListener('resize', this.handleResize)
},
        这回牛了!
五、考虑封装成组件化
子组件-瀑布流组件WaterfallFlow.vue
默认四列
this.$slots.default:我们获取子组件通过$slots插槽,需要注意的是子组件需要加ref(v-for也得加,不怕重复,因为当使用 v-for 时 ref 会被收集为数组)
本来想通过document.querySelectorAll 获取,但获取不到子组件slot的元素:
- 
作用域隔离:父组件传递的 class 会被 Vue 的 scoped CSS 自动添加哈希后缀
 - 
渲染时序:slot 内容渲染时机晚于子组件 mounted 钩子
 - 
DOM 位置:slot 内容实际存在于父组件的 DOM 树中
 
组件代码移步:传送门
使用的组件
            
            
              java
              
              
            
          
            <WaterfallFlow class="Visualization" childrenClass="Visualization-item" :columnCount="4" :gap="20" :itemWidth="23">
    <div class="Visualization-item" v-for="(card, cardIndex) in cardList" ref="flowItems" :key="cardIndex">
    </div>
  </WaterfallFlow>  
<style scoped lang='scss'>
.Visualization {
  gap: 30px;
  &-item {
    // 子组件写
    width: 23%;
    // height: 100%;
    position: absolute;
    left: 0;
    top: 0;
    // 添加GPU加速
    will-change: transform;
    // 优化过渡效果
    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }
}
</style>
        六、现有技术方案
当初要实现这样的布局都是依赖于JavaScript来实现,所以当时出现过很多实现瀑布流布局的插件。比如Masonry、Isotope等都是非常有名的插件。
有人尝试通过Multi-columns相关的属性column-count、column-gap配合break-inside来实现瀑布流布局
七、瀑布流布局的性能优化
减少重排和重绘
- 使用 
transform和opacity属性来实现动画效果。 - 减少对 DOM 的操作,例如 
DocumentFragment。 
图片优化
- 
使用WebP,它通常比 JPEG 或 PNG 格式更小,但保持相同的质量。
 - 
实施图片懒加载。
 - 
为图片设置宽度和高度属性,以避免页面在加载图片时发生布局变化。
 
使用虚拟化或分页
当瀑布流中的项目数量非常多时,渲染所有项目可能会对性能产生影响。
我是掘金Dignity_呱,至此撒花~
后记
希望本文对你有点帮助,或者引起思考。
我们在实际项目中或多或少遇到一些奇奇怪怪的问题。
自己也会对一些写法的思考,为什么不行🤔,又为什么行了?
最后,祝君能拿下满意的offer。
我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车
👍 如果对您有帮助,您的点赞是我前进的润滑剂。
以往推荐
前端哪有什么设计模式(12k+)
为什么没人用mixin(7k+)