该源码来自 uviewpro 地址为:https://uviewpro.cn/zh/components/waterfall.html
我改成vue2的写法 优化了计时器
瀑布流插件
<template><divclass="waterfull"><divclass="left"ref="leftRef"><slot name="left":leftList="leftData"></slot></div><divclass="right"ref="rightRef"><slot name="right":rightList="rightData"></slot></div></div></template><script>/** * * @author COOBY * @since 2026-01-26 14:19 */exportdefault{props:{list:{type:Array,default:()=>[]},addTime:{type:Number,default:200}},data(){return{timer:null,leftData:[],rightData:[],tempList:[],}},computed:{copyFlowList(){returnthis.deepClone(this.list);}},watch:{copyFlowList(nVal,oVal){conststartIndex=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:{asyncsplitData(){if(!this.tempList.length)return;if(!this.$refs.leftRef||!this.$refs.rightRef)return;awaitthis.$nextTick();constleftRect=this.$refs.leftRef.offsetHeight;constrightRect=this.$refs.rightRef.offsetHeight;// 如果左边小于或等于右边,就添加到左边,否则添加到右边constitem=this.tempList[0];// 解决多次快速上拉后,可能数据会乱的问题,因为经过上面的两个await节点查询阻塞一定时间,加上后面的定时器干扰// 数组可能变成[],导致此item值可能为undefinedif(!item)return;if(leftRect<rightRect){this.leftData.push(item);}elseif(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();// 如果临时数组还有数据,继续循环awaitthis.$nextTick();if(this.tempList.length){if(this.timer)clearTimeout(this.timer);this.timer=setTimeout(()=>{this.splitData();},Math.max(0,this.addTime));// 防止负数}},deepClone(obj,cache=newWeakMap()){if(obj===null||typeofobj!=='object')returnobj;if(cache.has(obj))returncache.get(obj);letclone;if(objinstanceofDate){clone=newDate(obj.getTime());}elseif(objinstanceofRegExp){clone=newRegExp(obj);}elseif(objinstanceofMap){clone=newMap(Array.from(obj,([key,value])=>[key,this.deepClone(value,cache)]));}elseif(objinstanceofSet){clone=newSet(Array.from(obj,value=>this.deepClone(value,cache)));}elseif(Array.isArray(obj)){clone=obj.map(value=>this.deepClone(value,cache));}elseif(Object.prototype.toString.call(obj)==='[object Object]'){clone=Object.create(Object.getPrototypeOf(obj));cache.set(obj,clone);for(const[key,value]ofObject.entries(obj)){clone[key]=this.deepClone(value,cache);}}else{clone=Object.assign({},obj);}cache.set(obj,clone);returnclone;}},}</script><style lang="less"scoped>.waterfull{display:flex;gap:2vw;width:92vw;margin:0auto;.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){conststartIndex=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
这是整个组件的灵魂函数:
asyncsplitData(){if(!this.tempList.length)return;if(!this.$refs.leftRef||!this.$refs.rightRef)return;awaitthis.$nextTick();// 确保 DOM 已更新constleftRect=this.$refs.leftRef.offsetHeight;constrightRect=this.$refs.rightRef.offsetHeight;constitem=this.tempList[0];if(!item)return;if(leftRect<=rightRect){this.leftData.push(item);}else{this.rightData.push(item);}this.tempList.shift();// 移除已处理项awaitthis.$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=newWeakMap()){// 处理 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>- 完全解耦渲染逻辑,父组件决定如何展示每一项;
- 支持任意内容(图片、卡片、文字等)。