🧳 我的 React Trip 之旅(3):瀑布流翻车实录——我是如何让图片乖乖排队的?

46 阅读3分钟

小红书式两列布局?别被“随机高度”骗了!我的图片差点叠成俄罗斯方块


🌊 开头:为什么我的瀑布流像“地震后的砖墙”?

那天,我信心满满地写了个瀑布流组件,心想:“不就是把图片分两列嘛,i % 2 一搞,完事!”

结果一跑起来——左边一列堆得像埃菲尔铁塔,右边矮得像乐高积木。用户往下滚,新图“啪”一下插进左边,整个页面抖得像在蹦迪。

设计师路过看了一眼,幽幽地说:“你这叫‘瀑布’?我看是‘泥石流’。”

我脸一红,决定痛改前非。


🧱 第一步:先让图片“站好队”——两列布局怎么搞?

很多人以为瀑布流要用 Masonry 库,其实纯 CSS + React 就够了

我的方案超简单:

  • 所有图片存在一个数组 images
  • 偶数索引(0,2,4…)→ 左列
  • 奇数索引(1,3,5…)→ 右列
<div className={styles.column}>
  {images.filter((_, i) => i % 2 === 0).map(img => (
    <ImageCard key={img.id} {...img} />
  ))}
</div>
<div className={styles.column}>
  {images.filter((_, i) => i % 2 === 1).map(img => (
    <ImageCard key={img.id} {...img} />
  ))}
</div>

CSS 更简单:

.wrapper {
  display: flex;
  justify-content: space-between;
  padding: 16px;
}
.column {
  width: 48%;
  margin: 0 1%;
  display: flex;
  flex-direction: column;
}

💡 为什么不用“哪列矮塞哪列”?
因为第一版先做对,再做好!奇偶拆分虽然不完美,但代码清晰、性能稳定,面试官一看就懂。


🔍 第二步:滚动到底?自动加载更多!

用户刷到快到底了,总不能让他手动点“加载更多”吧?那多 low!

于是,我祭出 IntersectionObserver ——浏览器原生的“盯梢高手”。

我在底部放了个“卧底”元素:

<div ref={loader} className={styles.loader}>加载中...</div>

然后派一个“观察者”盯着它:

useEffect(() => {
  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) {
      fetchMore(); // 去要下一页数据!
    }
    observer.unobserve(entry.target); // 看一眼就撤,避免重复触发
  });
  if (loader.current) observer.observe(loader.current);
  return () => observer.disconnect(); // 组件卸载时,彻底解散观察者
}, []);

✅ 为什么不用 scroll 事件?
因为 scroll 会疯狂触发,性能差;而 IntersectionObserver 是浏览器优化过的,只在元素进出视窗时通知你,省电又安静。


🧹 第三步:别忘了“打扫战场”——内存泄漏警告!

很多教程只教你怎么“观察”,却不说怎么“收摊”。

但我知道:前端工程师的修养,藏在 disconnect()

所以我在 useEffect 的清理函数里加了:

return () => observer.disconnect();

否则,用户切到其他页面,这个观察者还在后台偷偷运行——就像你出门忘了关煤气,迟早出事。


🎯 结尾:瀑布流稳了,但还能更好!

现在,我的瀑布流:

  • 图片分两列,不打架
  • 滚到底自动加载,不打断浏览
  • 内存安全,不拖累性能

但它还有提升空间:

  • 图片懒加载(先占位,滑到才加载)
  • 骨架屏(加载时显示灰色块,不白屏)
  • 动态列高分配(真正的小红书体验)

这些,留到下次升级再说。毕竟,旅行不是冲刺,而是慢慢走,稳稳看风景

下一站,我会带你搞定“搜索框”——如何让用户打字不卡顿?如何缓存历史记录?防抖 + useMemo + localStorage,一套组合拳打穿性能瓶颈!