让瀑布流不再"偏瘫":React动态高度分配方案详解

130 阅读3分钟

今天我就来分享一下在 React 中实现瀑布流布局的完整历程,包括踩过的坑和最终的优化方案。

初探瀑布流:最简单的奇偶排列

最开始我尝试用最直观的方式实现——按奇偶数列排列。具体思路是把数据分成两列,奇数项放左边,偶数项放右边:

function SimpleWaterfall({ items }) {
  const leftColumn = items.filter((_, index) => index % 2 === 0);
  const rightColumn = items.filter((_, index) => index % 2 === 1);

  return (
    <div className="waterfall-container">
      <div className="column">
        {leftColumn.map(item => (
          <WaterfallItem key={item.id} item={item} />
        ))}
      </div>
      <div className="column">
        {rightColumn.map(item => (
          <WaterfallItem key={item.id} item={item} />
        ))}
      </div>
    </div>
  );
}

对应的 CSS 也很简单:

.waterfall-container {
  display: flex;
  gap: 16px;
}

.column {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

这种实现确实简单粗暴,在小规模数据下看起来也不错。但当我用真实项目数据测试时,问题很快就暴露出来了...

问题浮现:极端情况下的布局灾难

在测试过程中,我遇到了两个极端情况:

  1. 连续多个高度相近的奇数项:导致左列明显比右列长很多
  2. 某个特别高的项目:如果这个高项目恰好排在某一列,整列会被拉得很长

举个例子,假设我们有以下高度数据(单位px):

[300, 150, 300, 150, 300, 150]

按照奇偶排列的结果是:

  • 左列:300, 300, 300(总高900)
  • 右列:150, 150, 150(总高450)

两列高度差达到了450px!这完全破坏了瀑布流应该保持的平衡美感。

image.png

解决方案:动态计算列高度

为了解决这个问题,我研究后发现需要动态计算列高度,始终把新项目添加到当前较矮的那一列。下面是改进后的实现:

function DynamicWaterfall({ items }) {
  const columnRefs = [useRef(null), useRef(null)];
  const [columns, setColumns] = useState([[], []]);

  useEffect(() => {
    const newColumns = [[], []];
    
    items.forEach(item => {
      // 获取两列当前高度
      const heights = columnRefs.map(ref => 
        ref.current?.clientHeight || 0
      );
      
      // 添加到较矮的列
      const targetCol = heights[0] <= heights[1] ? 0 : 1;
      newColumns[targetCol].push(item);
    });

    setColumns(newColumns);
  }, [items]);

  return (
    <div className="waterfall-container">
      {columns.map((column, index) => (
        <div key={index} ref={columnRefs[index]} className="column">
          {column.map(item => (
            <WaterfallItem key={item.id} item={item} />
          ))}
        </div>
      ))}
    </div>
  );
}

这个方案通过实时计算列高度,确保了两列的平衡。但很快我又发现了新的性能问题...

性能优化:避免不必要的计算

在快速滚动加载大量数据时,频繁的DOM查询(clientHeight)导致了明显的卡顿。于是我引入了两个优化:

  1. 使用ResizeObserver替代直接高度查询
  2. 记录高度状态避免重复计算

优化后的核心逻辑:

function OptimizedWaterfall({ items }) {
  const [columns, setColumns] = useState([[], []]);
  const [heights, setHeights] = useState([0, 0]);
  const observers = useRef([]);

  // 初始化ResizeObserver
  useEffect(() => {
    const callback = (entries) => {
      const newHeights = [...heights];
      entries.forEach(entry => {
        const index = observers.current.indexOf(entry.target);
        if (index !== -1) {
          newHeights[index] = entry.contentRect.height;
        }
      });
      setHeights(newHeights);
    };

    const observer = new ResizeObserver(callback);
    observers.current.forEach(el => observer.observe(el));
    return () => observer.disconnect();
  }, []);

  // 动态分配项目
  useEffect(() => {
    const newColumns = [[], []];
    items.forEach(item => {
      const targetCol = heights[0] <= heights[1] ? 0 : 1;
      newColumns[targetCol].push(item);
    });
    setColumns(newColumns);
  }, [items, heights]);

  // 省略渲染部分...
}

锦上添花:实现懒加载

最后,为了让长列表滚动更流畅,我加入了懒加载功能:

function LazyLoadWaterfall({ initialItems, fetchMore }) {
  const [items, setItems] = useState(initialItems);
  const loaderRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) {
        fetchMore().then(newItems => {
          setItems(prev => [...prev, ...newItems]);
        });
      }
    });

    if (loaderRef.current) {
      observer.observe(loaderRef.current);
    }

    return () => observer.disconnect();
  }, [fetchMore]);

  return (
    <>
      <OptimizedWaterfall items={items} />
      <div ref={loaderRef} className="loader">加载中...</div>
    </>
  );
}

经验总结

通过这次实践,我学到了几个重要经验:

  1. 简单的奇偶排列只适合高度均匀的项,真实项目必须考虑动态高度
  2. 直接查询DOM性能堪忧,ResizeObserver是更好的选择
  3. 懒加载能显著提升长列表性能,但要注意加载阈值
  4. React中的瀑布流需要状态驱动,纯CSS方案难以实现动态平衡

最终效果在我的个人项目中运行良好,即使加载上千项也能保持流畅。希望这篇文章能帮助到正在探索瀑布流布局的你!