前言
- 常网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+)