该源码来自 uviewpro 地址为:uviewpro.cn/zh/componen…
我改成vue2的写法 优化了计时器
瀑布流插件
<template>
<div class="waterfull">
<div class="left" ref="leftRef">
<slot name="left" :leftList="leftData"></slot>
</div>
<div class="right" ref="rightRef">
<slot name="right" :rightList="rightData"></slot>
</div>
</div>
</template>
<script>
/**
*
* @author COOBY
* @since 2026-01-26 14:19
*/
export default {
props: {
list: { type: Array, default: () => [] },
addTime: { type: Number, default: 200 }
},
data() {
return {
timer: null,
leftData: [],
rightData: [],
tempList: [],
}
},
computed: {
copyFlowList() {
return this.deepClone(this.list);
}
},
watch: {
copyFlowList(nVal, oVal) {
const startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0;
// 拼接上原有数据
this.tempList = this.tempList.concat(this.deepClone(nVal.slice(startIndex)));
this.splitData();
},
immediate: true
},
mounted() {
this.tempList = this.deepClone(this.copyFlowList);
this.splitData();
},
beforeDestroy() {
if (this.timer) clearTimeout(this.timer);
this.timer = null;
},
methods: {
async splitData() {
if (!this.tempList.length) return;
if (!this.$refs.leftRef || !this.$refs.rightRef) return;
await this.$nextTick();
const leftRect = this.$refs.leftRef.offsetHeight;
const rightRect = this.$refs.rightRef.offsetHeight;
// 如果左边小于或等于右边,就添加到左边,否则添加到右边
const item = this.tempList[0];
// 解决多次快速上拉后,可能数据会乱的问题,因为经过上面的两个await节点查询阻塞一定时间,加上后面的定时器干扰
// 数组可能变成[],导致此item值可能为undefined
if (!item) return;
if (leftRect < rightRect) {
this.leftData.push(item);
} else if (leftRect > rightRect) {
this.rightData.push(item);
} else {
// 这里是为了保证第一和第二张添加时,左右都能有内容
// 因为添加第一张,实际队列的高度可能还是0,这时需要根据队列元素长度判断下一个该放哪边
if (this.leftData.length <= this.rightData.length) {
this.leftData.push(item);
} else {
this.rightData.push(item);
}
}
// 移除临时列表的第一项
this.tempList.shift();
// 如果临时数组还有数据,继续循环
await this.$nextTick();
if (this.tempList.length) {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.splitData();
}, Math.max(0, this.addTime)); // 防止负数
}
},
deepClone(obj, cache = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (cache.has(obj)) return cache.get(obj);
let clone;
if (obj instanceof Date) {
clone = new Date(obj.getTime());
} else if (obj instanceof RegExp) {
clone = new RegExp(obj);
} else if (obj instanceof Map) {
clone = new Map(Array.from(obj, ([key, value]) => [key, this.deepClone(value, cache)]));
} else if (obj instanceof Set) {
clone = new Set(Array.from(obj, value => this.deepClone(value, cache)));
} else if (Array.isArray(obj)) {
clone = obj.map(value => this.deepClone(value, cache));
} else if (Object.prototype.toString.call(obj) === '[object Object]') {
clone = Object.create(Object.getPrototypeOf(obj));
cache.set(obj, clone);
for (const [key, value] of Object.entries(obj)) {
clone[key] = this.deepClone(value, cache);
}
} else {
clone = Object.assign({}, obj);
}
cache.set(obj, clone);
return clone;
}
},
}
</script>
<style lang="less" scoped>
.waterfull {
display: flex;
gap: 2vw;
width: 92vw;
margin: 0 auto;
.left,
.right {
flex: 1;
background-color: #fff;
height: fit-content;
}
}
</style>
vue中使用
<waterfull :list="videoList">
<template slot="left" slot-scope="{ leftList }">
<div v-for="(item, index) in leftList" :key="index">
<itemVideo :item="item"></itemVideo>
</div>
</template>
<template slot="right" slot-scope="{ rightList }">
<div v-for="(item, index) in rightList" :key="index">
<itemVideo :item="item"></itemVideo>
</div>
</template>
</waterfull>
一、需求目标
我们要实现一个组件,具备以下能力:
- 接收一个动态变化的
list数组(比如通过上拉加载新增数据); - 自动将新项“智能”分配到左列或右列,使两列高度尽可能平衡;
- 支持自定义每项添加的间隔时间(模拟“逐个加载”的动画效果);
- 避免因频繁更新导致的数据错乱或性能问题。
二、整体结构概览
<template>
<div class="waterfull">
<div class="left" ref="leftRef">
<slot name="left" :leftList="leftData"></slot>
</div>
<div class="right" ref="rightRef">
<slot name="right" :rightList="rightData"></slot>
</div>
</div>
</template>
- 使用
<slot>实现插槽分发,父组件可自由定义左右列的渲染方式; - 通过
ref获取左右容器的真实 DOM 高度,用于判断插入位置; - 数据分为
leftData和rightData两个数组,分别控制左右列内容。
三、核心逻辑拆解
1. 数据监听与增量处理
watch: {
copyFlowList(nVal, oVal) {
const startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0;
this.tempList = this.tempList.concat(this.deepClone(nVal.slice(startIndex)));
this.splitData();
},
immediate: true
}
copyFlowList是对props.list的深拷贝(避免直接修改原始数据);- 当
list变化时,只取新增部分(slice(startIndex)),避免重复处理已有项; - 新增项先存入
tempList临时队列,再交由splitData逐步分配。
✅ 为什么用临时队列?
因为我们希望“逐个”添加项(带时间间隔),而不是一次性塞入,这样能模拟真实加载过程,并防止 DOM 高度计算不准。
2. 智能分配算法:splitData
这是整个组件的灵魂函数:
async splitData() {
if (!this.tempList.length) return;
if (!this.$refs.leftRef || !this.$refs.rightRef) return;
await this.$nextTick(); // 确保 DOM 已更新
const leftRect = this.$refs.leftRef.offsetHeight;
const rightRect = this.$refs.rightRef.offsetHeight;
const item = this.tempList[0];
if (!item) return;
if (leftRect <= rightRect) {
this.leftData.push(item);
} else {
this.rightData.push(item);
}
this.tempList.shift(); // 移除已处理项
await this.$nextTick(); // 等待新项渲染完成,高度更新
if (this.tempList.length) {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.splitData();
}, Math.max(0, this.addTime));
}
}
分配策略详解:
| 条件 | 行为 |
|---|---|
leftHeight <= rightHeight | 新项放入左边 |
leftHeight > rightHeight | 新项放入右边 |
💡 为什么不是严格
<而是<=?
这样能确保第一项优先放入左边,第二项若高度相同(都为0),则进入else分支中的兜底逻辑。
兜底逻辑(初始状态处理):
// 当左右高度相等(如初始都为0)时
if (this.leftData.length <= this.rightData.length) {
this.leftData.push(item);
} else {
this.rightData.push(item);
}
- 防止前两项都塞到同一侧;
- 保证左右列都能有内容,提升首屏体验。
3. 异步调度与防抖
- 每次添加一项后,等待 DOM 渲染完成(
$nextTick())再计算下一次高度; - 使用
setTimeout控制添加频率(addTime默认 200ms); - 每次调用前
clearTimeout,防止多个定时器堆积(尤其在快速上拉加载时)。
⚠️ 注意:如果不加
$nextTick(),offsetHeight可能还是旧值,导致分配错误!
4. 深拷贝工具函数
deepClone(obj, cache = new WeakMap()) {
// 处理 null、基本类型、Date、RegExp、Map、Set、Array、Object 等
// 使用 WeakMap 防止循环引用
}
- 避免外部传入的对象被组件内部修改;
- 支持复杂嵌套结构,适用于大多数业务场景。
四、样式与布局
.waterfull {
display: flex;
gap: 2vw;
width: 92vw;
margin: 0 auto;
.left, .right {
flex: 1;
background-color: #fff;
height: fit-content; // 关键!让容器高度随内容增长
}
}
- 使用
flex: 1让左右列等宽; height: fit-content确保容器高度能被 JS 正确读取(offsetHeight依赖于此)。
五、使用示例
父组件中这样使用:
<waterfull :list="videoList">
<template slot="left" slot-scope="{ leftList }">
<div v-for="(item, index) in leftList" :key="index">
<itemVideo :item="item"></itemVideo>
</div>
</template>
<template slot="right" slot-scope="{ rightList }">
<div v-for="(item, index) in rightList" :key="index">
<itemVideo :item="item"></itemVideo>
</div>
</template>
</waterfull>
- 完全解耦渲染逻辑,父组件决定如何展示每一项;
- 支持任意内容(图片、卡片、文字等)。