返回首页

说说 useEffect 和 useLayoutEffect 的区别?

问题解析

面试官考察点:

  • 是否了解 useEffect 和 useLayoutEffect 的执行时机差异
  • 是否理解浏览器渲染流程
  • 能否根据场景选择合适的 Hook
  • 对 React 渲染机制的理解

核心概念

两个 Hook 的定义

  • useEffect:在浏览器绘制完成后异步执行
  • useLayoutEffect:在浏览器绘制之前同步执行 n

浏览器渲染流程

1. React 渲染(生成 Virtual DOM)
2. React 提交(Commit Phase)
   - 执行 useLayoutEffect
3. 浏览器绘制(Paint)
4. 执行 useEffect

详细解答

1. 基本用法对比

import React, { useEffect, useLayoutEffect, useRef } from 'react';

function Example() {
  const divRef = useRef();

  // useEffect - 异步执行,在绘制之后
  useEffect(() => {
    console.log('useEffect');
    console.log('DOM width:', divRef.current.offsetWidth);
  });

  // useLayoutEffect - 同步执行,在绘制之前
  useLayoutEffect(() => {
    console.log('useLayoutEffect');
    console.log('DOM width:', divRef.current.offsetWidth);
  });

  return <div ref={divRef}>Hello</div>;
}

// 输出顺序:
// 1. useLayoutEffect
// 2. useEffect

2. 执行时机差异

function TimingExample() {
  const [width, setWidth] = useState(0);
  const boxRef = useRef();

  useLayoutEffect(() => {
    // 在浏览器绘制前执行
    // 可以获取到 DOM 的最新布局信息
    console.log('useLayoutEffect - 同步执行');
    const newWidth = boxRef.current.getBoundingClientRect().width;

    // 同步修改状态,用户不会看到中间状态
    if (newWidth > 500) {
      setWidth(500); // 立即调整,避免闪烁
    }
  });

  useEffect(() => {
    // 在浏览器绘制后执行
    console.log('useEffect - 异步执行');
    // 可能看到布局闪烁
  });

  return <div ref={boxRef} style={{ width: width || '100%' }}>Content</div>;
}

3. 实际应用场景

useEffect 的典型场景

// 1. 数据获取
function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => setUsers(data));
  }, []);

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// 2. 订阅/取消订阅
function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();

    return () => {
      connection.disconnect();
    };
  }, [roomId]);

  return <div>Chat Room {roomId}</div>;
}

// 3. 手动修改 DOM(非布局相关)
function AutoFocusInput() {
  const inputRef = useRef();

  useEffect(() => {
    // 聚焦不需要在绘制前执行
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;
}

useLayoutEffect 的典型场景

// 1. 防止布局闪烁
function Tooltip({ children, content }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const tooltipRef = useRef();

  useLayoutEffect(() => {
    const rect = tooltipRef.current.getBoundingClientRect();
    const viewportWidth = window.innerWidth;

    // 如果 tooltip 超出视口右侧,调整到左侧显示
    if (rect.right > viewportWidth) {
      setPosition({ x: rect.left - rect.width, y: rect.top });
    }
  }, []);

  return (
    <div ref={tooltipRef} style={{ left: position.x, top: position.y }}>
      {content}
    </div>
  );
}

// 2. 测量 DOM 并同步修改
function MeasureExample() {
  const [height, setHeight] = useState(0);
  const contentRef = useRef();

  useLayoutEffect(() => {
    // 测量内容高度
    const measuredHeight = contentRef.current.scrollHeight;

    // 同步设置高度,避免动画过程中的闪烁
    setHeight(measuredHeight);
  });

  return (
    <div style={{ height, transition: 'height 0.3s' }}>
      <div ref={contentRef}>{/* 动态内容 */}</div>
    </div>
  );
}

// 3. 从 DOM 读取样式并同步修改
function ColorPicker() {
  const [color, setColor] = useState('#000');
  const canvasRef = useRef();

  useLayoutEffect(() => {
    // 从 canvas 获取像素颜色
    const ctx = canvasRef.current.getContext('2d');
    const pixel = ctx.getImageData(0, 0, 1, 1).data;

    // 同步更新颜色
    setColor(`rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`);
  }, []);

  return <canvas ref={canvasRef} />;
}

深入理解

渲染流程详解

function DetailedExample() {
  console.log('1. Render Phase - 计算组件输出');

  useLayoutEffect(() => {
    console.log('3. Layout Effect - 在绘制前同步执行');
    // 此时可以读取 DOM 布局信息
    // 同步修改状态会阻塞绘制
  });

  useEffect(() => {
    console.log('4. Passive Effect - 在绘制后异步执行');
    // 不会阻塞绘制
  });

  return (
    <div ref={() => console.log('2. Commit Phase - DOM 更新')}>
      Content
    </div>
  );
}

性能影响

// 阻塞渲染的示例(慎用)
function BlockingExample() {
  useLayoutEffect(() => {
    // 耗时操作会阻塞浏览器绘制
    const start = performance.now();
    while (performance.now() - start < 100) {
      // 阻塞 100ms
    }
  });

  return <div>Content</div>;
}

// 正确的做法:耗时操作放在 useEffect
function NonBlockingExample() {
  useEffect(() => {
    // 不会阻塞绘制
    const start = performance.now();
    while (performance.now() - start < 100) {
      // 阻塞 100ms
    }
  });

  return <div>Content</div>;
}

SSR 注意事项

import React, { useEffect, useLayoutEffect } from 'react';

// 在服务端渲染时,useLayoutEffect 会发出警告
// 解决方案:使用 useEffect 替代,或条件判断

const useIsomorphicLayoutEffect = typeof window !== 'undefined'
  ? useLayoutEffect
  : useEffect;

function SSRComponent() {
  // 安全地在 SSR 中使用
  useIsomorphicLayoutEffect(() => {
    // 只在客户端执行
    document.title = 'Updated';
  }, []);

  return <div>Content</div>;
}

最佳实践

1. 默认使用 useEffect

// 大多数情况下,useEffect 足够使用
function Component() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}

2. 仅在必要时使用 useLayoutEffect

// 当出现布局闪烁问题时,才考虑使用 useLayoutEffect
function Modal({ isOpen }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const modalRef = useRef();

  useLayoutEffect(() => {
    if (isOpen) {
      const rect = modalRef.current.getBoundingClientRect();
      const viewportHeight = window.innerHeight;

      // 确保 modal 在视口内
      if (rect.bottom > viewportHeight) {
        setPosition(prev => ({
          ...prev,
          top: viewportHeight - rect.height - 20
        }));
      }
    }
  }, [isOpen]);

  return (
    <div ref={modalRef} style={{ position: 'absolute', ...position }}>
      Modal Content
    </div>
  );
}

3. 避免在 useLayoutEffect 中执行耗时操作

// 错误:阻塞渲染
useLayoutEffect(() => {
  heavyComputation(); // 耗时计算
}, []);

// 正确:异步执行耗时操作
useEffect(() => {
  heavyComputation();
}, []);

对比总结

特性 useEffect useLayoutEffect
执行时机 绘制完成后异步执行 绘制前同步执行
阻塞渲染
适用场景 数据获取、订阅、事件处理 DOM 测量、防止闪烁
性能影响 大(可能阻塞)
SSR 支持 完整 需要特殊处理
使用频率 高(默认选择) 低(特殊情况)

面试要点

  1. 执行时机差异:useEffect 异步(绘制后),useLayoutEffect 同步(绘制前)
  2. 性能影响:useLayoutEffect 会阻塞浏览器绘制
  3. 适用场景:useEffect 用于大多数副作用,useLayoutEffect 用于布局相关
  4. SSR 注意:服务端渲染时 useLayoutEffect 会有警告

常见追问:

  • 什么时候应该使用 useLayoutEffect 而不是 useEffect?
  • useLayoutEffect 会阻塞渲染,为什么还需要它?
  • 在服务端渲染时如何处理 useLayoutEffect?
  • 如果在 useLayoutEffect 中 setState 会发生什么?