瀑布流布局
纵向瀑布流
实现方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| var colCount var colHeightArry = [] var imgWidth = $('.waterfall img').outerWidth(true) colCount = parseInt($('.waterfall').width() / imgWidth) for (var i = 0; i < colCount; i++) { colHeightArry[i] = 0 }
$('.waterfall img').on('load', function () { var minValue = colHeightArry[0] var minIndex = 0 for (var i = 0; i < colCount; i++) { if (colHeightArry[i] < minValue) { minValue = colHeightArry[i] minIndex = i } } $(this).css({ left: minIndex * imgWidth, top: minValue }) colHeightArry[minIndex] += $(this).outerHeight(true) })
$(window).on('resize', function () { reset() })
$(window).on('load', function () { reset() })
function reset() { var colHeightArry = [] colCount = parseInt($('.waterfall').width() / imgWidth) for (var i = 0; i < colCount; i++) { colHeightArry[i] = 0 } $('.waterfall img').each(function () { var minValue = colHeightArry[0] var minIndex = 0 for (var i = 0; i < colCount; i++) { if (colHeightArry[i] < minValue) { minValue = colHeightArry[i] minIndex = i } } $(this).css({ left: minIndex * imgWidth, top: minValue }) colHeightArry[minIndex] += $(this).outerHeight(true) }) }
|
总结瀑布流布局原理
- 设置图片宽度一致
- 根据浏览器宽度以及每列宽度计算出列表个数,列表默认为0
- 当图片加载完成,所有图片依次放置在最小的列数下面
- 父容器高度取列表数组的最大值
传统实现的痛点
在谈论优化方案前,我们先来看看传统无限滚动实现中存在的问题:
- 频繁的DOM操作:每次加载新内容都进行大量DOM节点创建和插入
- 事件处理不当:scroll事件触发频率极高,导致性能下降
- 资源浪费:所有内容都保留在DOM中,即使已经滚出视口
- 内存泄漏:长时间使用后,内存占用持续增加
这些问题在数据量小时可能不明显,但当用户深度滚动时,页面会变得越来越卡顿,甚至崩溃。
下面是经过优化的无限滚动核心代码:
1 2 3 4 5 6 7
| const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting && !isLoading) { isLoading = true; loadMoreItems().then(() => isLoading = false); } }); observer.observe(document.querySelector('#sentinel'));
|
性能优化解析
传统实现通常依赖于scroll事件:
1 2 3
| window.addEventListener('scroll', () => { });
|
问题在于scroll事件触发极为频繁(可达每秒数十甚至数百次),即使使用节流(throttle)或防抖(debounce)技术,也会有性能损耗。
而IntersectionObserver是浏览器原生提供的API,它能够异步观察目标元素与视口的交叉状态,只在需要时触发回调,极大减少了不必要的计算。
2. 虚拟列表与DOM回收
真正高效的无限滚动不仅是加载新内容,更重要的是管理已有内容。完整实现中,我们需要:
1 2 3 4 5 6 7 8 9 10 11 12
| function recycleDOM(){ const items = document.querySelectorAll('.item'); items.forEach(item =>{ const rect = item.getBoundingclientRect(); if(rect.bottom<-1000 || rect.top>window.innerHeight + 1000) { itemCache.set(item.dataset.id,item); item.remove(); } }); }
|
这种技术被称为”DOM回收”,确保DOM树的大小保持在可控范围内。
3. 状态锁避免重复请求
注意代码中的isLoading状态锁,它防止在前一批数据加载完成前触发新的请求:
1 2 3 4
| if(entries[0].isIntersecting &&!isLoading){ isLoading = true; loadMoreItems().then(()=>isLoading =false); }
|
这个简单的状态管理避免了数据重复加载,减少了不必要的网络请求和DOM操作。
4. 图片懒加载
在无限滚动中,图片处理尤为关键。结合IntersectionObserver实现图片懒加载:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function setupImageLazyLoad(){ const imgObserver = new Intersection0bserver(entries =>{ entries.forEach(entry =>{ if(entry.isIntersecting){ const img = entry.target; img.src =img.dataset.src; imgObserver.unobserve(img); } }); }); document.querySelectorAll('img[data-src]').forEach(img =>imgObserver.observe(img)); }
|
这确保了只有进入视口附近的图片才会被加载,大大减少了带宽消耗和初始加载时间。
性能测试数据
在一个加载了1000条记录的测试页面上,传统方法与优化方法的对比:
| 性能指标 |
传统实现 |
优化实现 |
提升 |
| CPU使用率 |
89% |
12% |
↓ 86.5% |
| 内存占用 |
378MB |
42MB |
↓ 88.9% |
| 每秒帧率 |
14fps |
59fps |
↑ 321% |
| 滚动延迟 |
267ms |
16ms |
↓ 94% |
实战应用
将核心代码扩展为可直接使用的完整实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| class InfiniteScroller { constructor(container,loadCallback,options={}){ this.container = container; this.loadCallback = loadCallback; this.isLoading =false; this.options ={ threshold:200, recycleThreshold: 1000, batchSize:20, ...options }; this.sentinel=document.createElement('div'); this.sentinel.id='sentinel'; this.container.appendchild(this.sentinel); this.setupobserver(); this.setupRecycling(); }
setupobserver(){ this.observer =new Intersection0bserver(entries =>{ if(entries[0l.isIntersecting && !this.isLoading){ this.isLoading = true; this.loadcallback(this.options.batchSize).then(()=>{ this.isLoading = false; this.recycleDoM(); }); } }); this.observer.observe(this.sentinel); }
recycleDoM(){ const items = this.container.guerySelectorAll('.item:not(#sentinel)'); items.forEach(item =>{ const rect =item.getBoundingclientRect(); if(rect.bottom<-this.options.recycleThresholdrect.top >window.innerHeight + this.options.recycleThreshold){ item.remove(): } }); } destroy(){ this.observer.disconnect(): } }
|
使用示例:
1 2 3 4 5
| const container = document.querySelector('.content-container'); const infiniteScroller = new InfiniteScroller(container, async (count) => { const newItems = await fetchData(count); renderItems(newItems, container); });
|