鲁斯前端布鲁斯前端

文章中英模式

布鲁斯前端React面试题目 - useEffect 与 useLayoutEffect的使用选择

深入比较useEffect与useLayoutEffect的执行时机、适用场景与效能影响。了解两者在React渲染流程中的区别,正确选择何时使用useLayoutEffect来避免闪烁问题,以及如何在SSR环境中正确处理。

影片縮圖

懒得看文章?那就来看视频吧

两者执行时机的根本区别

useEffectuseLayoutEffect的API完全相同,但它们在React渲染流程中的执行时机不同:

Hook执行时机同步/异步画面更新时机
useEffect渲染后,浏览器绘制后异步用户先看到更新,然后执行effect
useLayoutEffect渲染后,浏览器绘制前同步执行完effect后,用户才看到更新

渲染流程对比图:

useEffect的流程:

  1. 1. React更新虚拟DOM并计算差异
  2. 2. React更新实际DOM
  3. 3. 浏览器绘制画面(用户看到更新)
  4. 4. 然后执行useEffect回调

useLayoutEffect的流程:

  1. 1. React更新虚拟DOM并计算差异
  2. 2. React更新实际DOM
  3. 3. 执行useLayoutEffect回调
  4. 4. 浏览器绘制画面(用户看到更新)
// 基本语法相同,但执行时机不同
import { useEffect, useLayoutEffect } from 'react';

function ExampleComponent() {
  const [state, setState] = useState(initialState);
  
  // 在画面绘制后执行(非阻塞)
  useEffect(() => {
    // 这里的代码在画面更新后执行
    // 即使执行耗时操作,用户也已经看到了UI
  }, [dependencies]);
  
  // 在画面绘制前执行(阻塞)
  useLayoutEffect(() => {
    // 这里的代码在DOM更新后但画面绘制前执行
    // 在这里的操作会阻塞画面更新,直到执行完成
  }, [dependencies]);
  
  return <div>...</div>;
}

两者的适用场景对比

简单来说,useEffectuseLayoutEffect的选择取决于你需要副作用何时执行:

Hook适用场景实际例子
useEffect
推荐优先使用
  • 1. 数据获取
  • 2. 事件监听
  • 3. 订阅设置
  • 4. 计时器操作
  • 5. 日志记录
  • 1. API请求获取用户数据
  • 2. 监听窗口大小变化
  • 3. 设置WebSocket连接
  • 4. 定时更新在线状态
useLayoutEffect
特定场景使用
  • 1. DOM测量
  • 2. 视觉动画初始化
  • 3. 防止闪烁
  • 4. 元素定位
  • 1. 测量元素高度后调整布局
  • 2. 工具提示精确定位
  • 3. 滚动位置调整
  • 4. 拖拽组件初始化

记住: 除非你需要在画面绘制前进行DOM操作(避免闪烁或需要精确测量),否则应该使用useEffectuseLayoutEffect会阻塞渲染,可能影响性能。

对性能的影响

选择合适的hook会对应用的性能产生明显影响:

useEffect的性能特性

  • 1. 非阻塞渲染 - 画面更新不会等待effect完成
  • 2. 允许浏览器先绘制UI再执行耗时操作
  • 3. 对于大多数副作用来说更高效
  • 4. 可能导致视觉闪烁(如果effect改变了可见元素)

useLayoutEffect的性能特性

  • 1. 阻塞渲染 - 会延迟画面更新直到effect完成
  • 2. 在effect中的耗时操作会导致明显的UI延迟
  • 3. 避免了闪烁问题
  • 4. 对于需要同步DOM更新的操作更合适
// 性能影响案例:闪烁问题

// ❌ 使用useEffect可能导致闪烁
function FlickeringExample() {
  const [width, setWidth] = useState(0);
  const divRef = useRef();
  
  useEffect(() => {
    // 问题:先渲染初始状态,用户看到这一帧
    // 然后才测量并更新,导致内容跳动
    setWidth(divRef.current?.getBoundingClientRect().width || 0);
  }, []);

  return <div ref={divRef} style={{ width: `${width}px` }}>内容可能闪烁</div>;
}

// ✅ 使用useLayoutEffect避免闪烁
function SmoothExample() {
  const [width, setWidth] = useState(0);
  const divRef = useRef();
  
  useLayoutEffect(() => {
    // 优点:在画面绘制前完成测量和更新
    // 用户只会看到最终结果,不会看到中间过程
    setWidth(divRef.current?.getBoundingClientRect().width || 0);
  }, []);

  return <div ref={divRef} style={{ width: `${width}px` }}>内容不会闪烁</div>;
}

实用例子:何时选择每一种

使用 useLayoutEffect 的典型例子

// 例子1:弹出提示定位 - 在画面绘制前计算位置
function Tooltip({ text, targetRef }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const tooltipRef = useRef(null);
  
  useLayoutEffect(() => {
    // 获取目标元素和提示框的尺寸
    // 计算提示框位置(置于目标元素上方中央)
    // 立即更新位置状态,确保渲染前完成
    if (targetRef.current && tooltipRef.current) {
      const targetRect = targetRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();
      setPosition({
        top: targetRect.top - tooltipRect.height - 10,
        left: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2
      });
    }
  }, [targetRef]);
  
  return <div ref={tooltipRef} style={{position: 'fixed', top: `${position.top}px`, left: `${position.left}px`}}>{text}</div>;
}

// 例子2:滚动到特定元素 - 避免闪烁
function ScrollToElement({ elementId }) {
  useLayoutEffect(() => {
    // 找到目标元素并立即滚动到该位置
    // 在画面绘制前执行,用户不会看到中间过程
    const element = document.getElementById(elementId);
    if (element) element.scrollIntoView({ behavior: 'smooth' });
  }, [elementId]);
  
  return null;
}

使用 useEffect 的典型例子

// 例子1:数据读取
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // 非阻塞操作,用户可以先看到加载状态
    setLoading(true);
    
    fetchUser(userId)
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        console.error(error);
        setLoading(false);
      });
  }, [userId]);
  
  if (loading) return <div>加载中...</div>;
  if (!user) return <div>找不到用户</div>;
  
  return <div>{user.name}</div>;
}

SSR (服务器端渲染) 的特殊考量

在服务器端渲染(SSR)环境中,useLayoutEffect会显示警告并且不会执行,因为服务器上没有「布局阶段」。这是因为:

  • 1. 服务器没有浏览器环境,无法进行DOM测量或更新
  • 2. SSR过程中不存在「浏览器绘制」的概念
  • 3. 服务器上的React只渲染出HTML字符串,不涉及实际DOM操作

处理SSR环境中的useLayoutEffect

// 方法1:条件性使用不同的hook
import { useEffect, useLayoutEffect } from 'react';

// 根据环境选择适当的hook
const useIsomorphicLayoutEffect = 
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

function MyComponent() {
  useIsomorphicLayoutEffect(() => {
    // 在浏览器中,这会表现得像useLayoutEffect
    // 在服务器上,这会表现得像useEffect(实际上不执行)
    // ...
  }, []);
  
  return <div>...</div>;
}

// 方法2:将DOM操作延迟到客户端
function SSRSafeComponent() {
  const [isMounted, setIsMounted] = useState(false);
  
  // 在客户端首次渲染时设置为已挂载
  useEffect(() => {
    setIsMounted(true);
  }, []);
  
  // 只在客户端渲染时才执行DOM操作
  useLayoutEffect(() => {
    // 这个effect只会在客户端执行
    if (isMounted) {
      // 安全进行DOM测量和操作...
    }
  }, [isMounted]);
  
  return <div>...</div>;
}

🔥 常见面试题目

(一) useEffect和useLayoutEffect的主要区别是什么?

解答: 两者的主要区别在于执行时机:

useEffect

• 异步执行

• 画面更新后执行

• 不阻塞渲染

useLayoutEffect

• 同步执行

• 画面更新前执行

• 阻塞渲染

两者API完全相同,只是执行时机不同,适用于不同场景。

(二) 为什么大多数情况下应该优先使用useEffect?

解答: 优先使用useEffect的原因:

  • 1. 性能更好:不阻塞画面更新
  • 2. 适合大多数场景:数据获取、订阅设置等不需要阻塞UI
  • 3. SSR友好:服务器端渲染环境中表现更一致
  • 4. 避免卡顿:耗时操作不会延迟UI显示

(三) 什么情况下应该使用useLayoutEffect?

解答: 适合useLayoutEffect的场景:

防止闪烁

元素位置或样式需要在用户看到前调整

DOM测量与定位

工具提示、弹出层需要精确定位

动画初始化

设置正确初始状态避免跳动

滚动位置控制

在页面显示前调整滚动位置

// 避免闪烁的简单例子
function Tooltip({ text, targetRef }) {
  const tooltipRef = useRef(null);
  
  useLayoutEffect(() => {
    // 测量目标元素位置
    const targetRect = targetRef.current.getBoundingClientRect();
    // 在画面更新前定位提示框
    tooltipRef.current.style.top = targetRect.bottom + 'px';
    tooltipRef.current.style.left = targetRect.left + 'px';
  }, []);
  
  return <div ref={tooltipRef}>{text}</div>;
}

(四) 如何在SSR环境中安全使用useLayoutEffect?

解答: 服务器没有DOM,无法执行布局效果。解决方案:

  1. 1. 创建同构hook,根据环境选择不同hook
  2. 2. 使用客户端检测,确保只在浏览器执行
// 同构hook方案
const useIsomorphicLayoutEffect = 
  typeof window !== 'undefined' 
    ? useLayoutEffect 
    : useEffect;

// 客户端检测方案
function SafeComponent() {
  const [isBrowser, setIsBrowser] = useState(false);
  
  useEffect(() => {
    setIsBrowser(true);
  }, []);
  
  useLayoutEffect(() => {
    if (isBrowser) {
      // 安全进行DOM操作
    }
  }, [isBrowser]);
  
  return <div>...</div>;
}

最佳实践:将需要布局测量的代码分离到纯客户端组件中。