作者:watermelo37
CSDN万粉博主、华为云云享专家、阿里云专家博主、腾讯云、支付宝合作作者,全平台博客昵称watermelo37。
一个假装是giser的coder,做不只专注于业务逻辑的前端工程师,Java、Docker、Python、LLM均有涉猎。
---------------------------------------------------------------------
温柔地对待温柔的人,包容的三观就是最大的温柔。
---------------------------------------------------------------------
在进行非完全标准化数据的可视化展示时,瀑布流是一种经常被采用的展示方法。瀑布流能够有效地将不同大小规格的内容以一种相对规整的方式呈现出来,尤其在处理海量数据时,依然能够保持出色的展示效果,给人一种杂而不乱、乱中有序的积极感受。
举几个例子,像小红书、淘宝、京东、千图网等平台,都采用了这种布局方式。固定列数、大量元素、每个元素高度各不相同的情况,就是我们所说的瀑布流布局。
实际在开发中,瀑布流离不开的一个情况就是海量数据,那么应对海量数据最好的设计模式是加入懒加载和无限滚动,但是做无限滚动还要同时做好页面的优化(即DOM的产生、销毁与复现策略),否则在滚动的过程中页面DOM不断堆砌,越来越多,会导致内存泄漏,严重的时候会导致页面崩溃。
以前想要实现这些内容非常麻烦,现在我们可以使用腾讯云提供的免费满血版deepseek来快速搭建一个无限滚动+懒加载+瀑布流的模块,用到即赚到。
进入腾讯云大模型知识引擎体验中心:https://lke.cloud.tencent.com/lke#/experience-center/home
没注册账号的注册一下,我这里是已经注册后的效果。
找到deepseek联网助手,点击立即体验:
这里我们问一下瀑布流是什么来测试,看看deepseek-R1模型提供的回答。
在正式提问之前,我们要先做好顶层设计。请注意:AI工具只能当做顾问,不能当做专家。当你无法理解AI,无法驾驭AI的时候,请先慢下来,专注于自身思维的提升。
首先,我们要搞清楚这个模块实现的难点在哪:
①新元素的加载时机:新元素什么时候加载?那肯定是某一列上一个元素尾部到达某个界限的时候,这个界限可以是视口的最底部,也可以是视口底部再往下一个固定数值(比如视口底部往下1000px,这样是为了提前加载图片内容优化用户体验)
②无限滚动带来的内存泄漏问题:在元素不断加载的过程中,页面中累积的元素会越来越多,造成的内存泄露问题也越来越大, 直至页面崩溃,所以很多网站为了避免这个问题,在做了瀑布流的同时拒绝实现无限滚动。那要怎么避免内存泄漏的问题呢?我们可以记录一个位置,比如视口上方1000px,当元素的顶部随着滚动到达视口上方1000px位置的时候,就记录该元素的元数据信息,然后销毁该DOM。这样就能避免DOM元素无限堆砌,同时记录的元数据信息(id、url、标题等重要数据)可以在用户回滚的时候重现这些元素。
也就是说,我们要:
基于DOM元素与视口的空间关系来判断新元素添加、销毁与重建的时机,同时监测元素与视口顶部的距离,超过预定阈值,就记录元素元数据并销毁DOM(这是为了在用户回滚的时候依然能按顺序生成销毁的DOM元素),以避免内存泄漏。
先观察思考过程判断Deepseek是否跑偏,是否按照我们的顶层设计来实现需求。
再来分析deepseek提供的设计思路:
维护两个数据池:
virtualPool
:存储所有已加载的元数据(包括尺寸、位置等信息)visibleItems
:当前视口内实际渲染的DOM元素集合interface VirtualItem {
id: string
data: any
height: number
top: number
column: number
}
const virtualPool = ref<VirtualItem[]>([])
const visibleItems = ref<Set<string>>(new Set())
使用CSS Grid创建自适应列布局,通过ResizeObserver动态调整列数:
<template>
<div class="waterfall-container" ref="container">
<div v-for="col in columns" :key="col" class="column" />
</div>
</template>
<script setup>
const container = ref<HTMLElement>()
const columns = ref(3)
onMounted(() => {
const observer = new ResizeObserver(entries => {
columns.value = Math.floor(entries[0].contentRect.width / 300)
})
observer.observe(container.value)
})
</script>
Intersection Observer配置,创建双阈值观察器(顶部/底部):
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const id = entry.target.dataset.id
if (entry.isIntersecting) {
visibleItems.value.add(id)
} else {
if (entry.boundingClientRect.top < entry.rootBounds!.top) {
// 元素离开视口顶部时记录位置并销毁
recordPosition(id)
visibleItems.value.delete(id)
}
}
})
}, {
root: null,
rootMargin: '200px 0px',
threshold: 0.1
})
DOM生命周期管理,通过动态组件实现条件渲染:
<template v-for="col in columns" :key="col" class="column">
<div v-for="item in getColumnItems(col)" :key="item.id">
<component
:is="visibleItems.has(item.id) ? 'RealComponent' : 'Placeholder'"
:data-id="item.id"
:data="item.data"
/>
</div>
</template>
滚动事件节流处理使用requestAnimationFrame
优化性能:
const checkScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement
if (scrollHeight - (scrollTop + clientHeight) < 500) {
loadMoreItems()
}
}
window.addEventListener('scroll', () => {
requestAnimationFrame(checkScroll)
})
数据分页加载
const loadMoreItems = async () => {
const lastItem = virtualPool.value[virtualPool.value.length - 1]
const newItems = await fetchItems({
offset: virtualPool.value.length,
position: lastItem?.top || 0
})
newItems.forEach(item => {
virtualPool.value.push({
...item,
height: 0, // 初始占位高度
top: calculateInitialPosition(item)
})
})
}
使用Map结构存储已销毁元素的位置信息:
const positionCache = new Map<string, { top: number, column: number }>()
const recordPosition = (id: string) => {
const element = document.querySelector(`[data-id="${id}"]`)
if (element) {
positionCache.set(id, {
top: element.getBoundingClientRect().top + window.scrollY,
column: parseInt(element.parentElement!.dataset.column!)
})
}
}
基于历史数据预测未加载元素的高度:
const predictHeight = (dataType: string) => {
const history = virtualPool.value
.filter(item => item.data.type === dataType)
.map(item => item.height)
return history.length > 0
? Math.ceil(history.reduce((a,b) => a+b)/history.length)
: 200 // 默认高度
}
如果还有更多需求,继续人机交互完善代码即可,核心是我提供的思路+强力的AI工具,剩下的就是时间问题。
<template>
<div class="masonry-container" ref="container">
<div v-for="(col, index) in columns" :key="index" class="masonry-column" :style="{ width: columnWidth + 'px', marginRight: gutter + 'px' }">
<div v-for="item in columnItems[index]" :key="item._uid" :class="itemClass" :style="{ transition: `all ${transitionDuration}ms ease`, transform: `translateY(${item.y}px)` }" transitionend="handleTransitionEnd(item)">
<slot name="item" :item="item.data"></slot>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
export default {
props: {
items: { type: Array, required: true },
columns: { type: Number, default: 2 },
gutter: { type: Number, default: 8 },
breakpoints: { type: Object, default: () => ({}) },
resizeObserver: { type: Boolean, default: true },
useImageLoader: { type: Boolean, default: true },
itemClass: { type: String, default: 'masonry-item' },
transitionDuration: { type: Number, default: 300 }
},
emits: ['layout-complete', 'item-positioned'],
setup(props, { emit }) {
const container = ref(null)
const columnHeights = ref([])
const columnItems = ref([])
const activeColumns = ref(props.columns)
const observer = ref(null)
// 计算列宽
const columnWidth = computed(() => {
if (!container.value) return 0
const totalGutter = (activeColumns.value - 1) * props.gutter
return (container.value.offsetWidth - totalGutter) / activeColumns.value
})
// 响应式断点处理
const handleBreakpoints = () => {
const breakpoints = Object.entries(props.breakpoints)
.sort((a, b) => b[0] - a[0])
const width = window.innerWidth
for (const [bp, cols] of breakpoints) {
if (width >= bp) {
activeColumns.value = cols
return
}
}
activeColumns.value = props.columns
}
// 布局核心算法
const layoutItems = () => {
columnHeights.value = new Array(activeColumns.value).fill(0)
columnItems.value = new Array(activeColumns.value).fill([])
props.items.forEach(item => {
const minHeight = Math.min(...columnHeights.value)
const columnIndex = columnHeights.value.indexOf(minHeight)
const newItem = {
...item,
y: minHeight,
_uid: Math.random().toString(36).substr(2, 9)
}
columnItems.value[columnIndex] = [
...columnItems.value[columnIndex],
newItem
]
// 触发单个元素定位事件
emit('item-positioned', {
element: newItem,
position: {
x: columnIndex * (columnWidth.value + props.gutter),
y: minHeight
}
})
// 更新列高度(假设已获取元素高度)
columnHeights.value[columnIndex] += item._height + props.gutter
})
// 触发布局完成事件
emit('layout-complete', {
columns: activeColumns.value,
containerHeight: Math.max(...columnHeights.value)
})
}
// 图片加载处理
const loadImages = () => {
if (!props.useImageLoader) return
props.items.forEach(item => {
const img = new Image()
img.src = item.image
img.onload = () => {
item._height = (columnWidth.value * img.height) / img.width
layoutItems()
}
})
}
// 初始化
onMounted(() => {
handleBreakpoints()
loadImages()
layoutItems()
if (props.resizeObserver) {
observer.value = new ResizeObserver(() => {
handleBreakpoints()
layoutItems()
})
observer.value.observe(container.value)
}
window.addEventListener('resize', handleBreakpoints)
})
onBeforeUnmount(() => {
if (observer.value) observer.value.disconnect()
window.removeEventListener('resize', handleBreakpoints)
})
// 监听相关变化
watch(() => props.items, layoutItems)
watch(activeColumns, layoutItems)
return {
container,
columnWidth,
columnItems
}
}
}
</script>
<style>
.masonry-container {
position: relative;
display: flex;
justify-content: flex-start;
}
.masonry-column {
transition: all 0.3s ease;
}
.masonry-item {
position: absolute;
width: 100%;
will-change: transform;
}
</style>
<template>
<MasonryLayout
:items="items"
@layout-complete="handleLayoutComplete"
>
</MasonryLayout>
</template>
<script setup>
import { onMounted } from 'vue'
const handleLayoutComplete = ({ columns, containerHeight }) => {
console.log(`当前列数:${columns},容器高度:${containerHeight}px`)
}
// 滚动加载更多
window.addEventListener('scroll', () => {
if (nearBottom()) {
items.value = [...items.value, ...newItems]
}
})
</script>
只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
其他热门文章,请关注:
极致的灵活度满足工程美学:用Vue Flow绘制一个完美流程图
你真的会使用Vue3的onMounted钩子函数吗?Vue3中onMounted的用法详解
通过array.filter()实现数组的数据筛选、数据清洗和链式调用
通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能
TreeSize:免费的磁盘清理与管理神器,解决C盘爆满的燃眉之急
通过MongoDB Atlas 实现语义搜索与 RAG——迈向AI的搜索机制
深入理解 JavaScript 中的 Array.find() 方法:原理、性能优势与实用案例详解
el-table实现动态数据的实时排序,一篇文章讲清楚elementui的表格排序功能
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver
JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、DOM操作等
高效工作流:用Mermaid绘制你的专属流程图;如何在Vue3中导入mermaid绘制流程图
Dockerfile全面指南:从基础到进阶,掌握容器化构建的核心工具
在线编程实现!如何在Java后端通过DockerClient操作Docker生成python环境
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。