React useEffect 与 useLayoutEffect 面试题整理

核心思路(一句话)

默认优先使用 useEffect;只有当你需要"读取布局信息并同步修改界面,以避免浏览器绘制出现中间状态或视觉闪烁"时,才使用 useLayoutEffect。

主要矛盾:

useLayoutEffect 在 DOM 变更之后、浏览器绘制之前同步执行,会阻塞绘制,但能保证视觉一致性

useEffect在提交后异步执行,通常不阻塞绘制,但可能出现短暂中间态

次要矛盾:

服务端渲染兼容、并发模式调度、性能开销、依赖数组与清理函数正确性、第三方库初始化时机。


1. 核心原理流程图(文本版)

text 复制代码
【一次 React 更新的完整流程】

  setState / 首次渲染
        │
        ▼
  Render 阶段(计算新的虚拟 DOM,可被打断/并发)
        │
        ▼
  Commit 阶段(把 DOM 变更真正写入页面,不可打断)
        │
        ├──────────── 同步 ─────────────┐
        ▼                               │
  ① useInsertionEffect 执行              │  ← React 18 新增,供 CSS-in-JS 库注入样式
  ② useLayoutEffect 的 cleanup 执行      │
  ③ useLayoutEffect 的回调【同步执行】    │
     (若其中调用 setState →             │
       React 立即同步重渲染并重走①②③)   │
        │                               │
        ▼                               │
  ★ 浏览器进行布局(Layout)+ 绘制(Paint)★ ← 用户此刻才看到画面
        │                               │
        ├──────────── 异步(调度器调度) ─┘
        ▼
  ④ useEffect 的 cleanup 执行
  ⑤ useEffect 的回调【异步执行】
     (若其中调用 setState → 触发新一轮异步更新)
        │
        ▼
  浏览器下一次绘制(用户看到二次更新结果)

闪烁的产生(用 useEffect 改 DOM):

text 复制代码
DOM 更新 → 浏览器绘制【用户看到未调整的中间态】 → useEffect 执行 → 再次更新 DOM → 再次绘制【用户看到最终态】
                     └────────── 两帧之间的不一致 = 闪烁 ──────────┘

无闪烁(用 useLayoutEffect 改 DOM):

text 复制代码
DOM 更新 → useLayoutEffect 同步读取尺寸并修改 DOM(必要时同步重渲染) → 浏览器一次性绘制【用户只看到最终态】

2. 面试题与答案

Q1:useEffect 和 useLayoutEffect 有什么区别?

核心思路: 区别不在"能不能做副作用",而在"执行时机"和"是否阻塞浏览器绘制"。

答案:

  1. 执行时机不同

    • useLayoutEffect:在 React 完成 DOM 变更后、浏览器绘制前,同步执行。
    • useEffect:在 React 提交完成后,异步调度执行,通常发生在浏览器绘制后。
  2. 是否阻塞绘制不同

    • useLayoutEffect:会阻塞浏览器绘制。如果里面执行耗时任务,用户会感觉页面卡顿。
    • useEffect:不会阻塞浏览器首次绘制。用户通常先看到界面,再执行副作用。
  3. 使用场景不同

    • useEffect:数据请求、订阅、事件监听、定时器、日志、滚动高亮、手动聚焦等大多数副作用。
    • useLayoutEffect:读取布局信息并同步修改 DOM 或状态,避免视觉闪烁。例如 Tooltip 定位、测量元素尺寸、第三方库需要 DOM 尺寸初始化。
  4. 服务端渲染表现不同

    • useLayoutEffect 在服务端渲染时不会执行,并且 React 会给出警告,因为服务端没有真实 DOM。
    • useEffect 可以在服务端渲染中安全使用,但也不会在服务端执行,而是在客户端执行。
  5. 经验法则

    • 默认用 useEffect
    • 如果 useEffect 导致视觉闪烁、布局跳动、用户看到中间状态,再改用 useLayoutEffect
    • 不要滥用 useLayoutEffect,因为它是同步阻塞的。

使用场景:

Hook 典型场景
useEffect 请求接口、设置订阅、添加事件监听、清除定时器、日志上报、滚动监听
useLayoutEffect Tooltip 定位、Popover 定位、测量 DOM 宽高、根据尺寸同步设置样式、避免闪烁
useInsertionEffect CSS-in-JS 库插入样式,业务代码一般不直接使用

边界场景:

  • 服务端渲染:useLayoutEffect 会警告。
  • 并发模式:useEffect 可能被延迟执行,useLayoutEffect 仍然同步执行。
  • 性能敏感:useLayoutEffect 中不要做重计算、大量 DOM 读写、频繁 setState
  • 依赖数组:空数组、无依赖数组、依赖项变化,行为不同。

Q2:为什么 useLayoutEffect 会阻塞浏览器绘制?

核心思路: 它运行在 Commit 阶段的 Layout 阶段,位于浏览器 Paint 之前,所以浏览器必须等它执行完才能绘制。

答案:

React 的提交阶段大致是:

text 复制代码
Render 阶段:计算变更
Commit 阶段:
  Mutation:更新真实 DOM
  Layout:执行 useLayoutEffect
浏览器绘制 Paint
Passive Effects:执行 useEffect

useLayoutEffect 在 Layout 阶段同步执行,因此:

  • 它能读取到最新的 DOM 布局。
  • 它能在浏览器绘制前修改 DOM 或触发同步重渲染。
  • 浏览器会等它完成后才绘制,所以用户不会看到中间状态。
  • 代价是:如果它执行时间长,浏览器绘制会被推迟,页面可能卡顿。

Q3:什么时候必须使用 useLayoutEffect?

核心思路: 当副作用需要"读取布局信息,并同步更新界面,避免闪烁"时,使用 useLayoutEffect

典型场景:

  1. Tooltip / Popover 定位

    • 需要先渲染出元素,读取它的尺寸和位置,再计算最终坐标。
    • 如果用 useEffect,用户可能先看到位置错误的元素,然后它跳到正确位置。
    • useLayoutEffect,浏览器绘制前就完成定位,用户只看到最终位置。
  2. 测量元素宽高并同步设置另一个元素

    • 例如左侧栏宽度变化,右侧栏需要同步调整。
    • 如果异步执行,可能出现短暂错位。
  3. 第三方库需要真实 DOM 尺寸初始化

    • 某些图表库、拖拽库、虚拟滚动库需要在绘制前拿到尺寸。
    • 如果库本身支持异步尺寸更新,也可以考虑 useEffect
  4. 避免首屏布局跳动

    • 例如根据容器宽度决定显示一行还是两行。
    • 如果先显示错误布局再修正,用户会看到闪烁。

边界:

  • 不要把所有 DOM 操作都放进 useLayoutEffect
  • 如果只是聚焦输入框、设置标题、添加事件监听,通常 useEffect 足够。
  • 如果操作耗时长,优先考虑是否能用 CSS、ResizeObserver、requestAnimationFrame 或库自身能力解决。

Q4:数据请求、订阅、事件监听应该用哪个?

核心思路: 这些操作通常不需要在浏览器绘制前完成,优先使用 useEffect

答案:

应该用 useEffect

原因:

  1. 数据请求是异步的

    • 请求接口不应该阻塞页面首次绘制。
    • 用户可以先看到加载状态,再看到数据。
  2. 订阅和事件监听不需要同步绘制

    • 例如监听 Redux store、窗口滚动、窗口尺寸变化。
    • 这些操作晚一点执行不会造成视觉闪烁。
  3. 清理函数很重要

    • 组件卸载或依赖变化时,需要取消请求、清除订阅、移除监听、清除定时器。
    • 否则会造成内存泄漏或状态更新到已卸载组件。

示例场景:

  • 获取用户信息:useEffect
  • 监听滚动实现高亮:useEffect
  • 监听窗口大小变化:useEffect
  • 设置定时器:useEffect
  • 手动聚焦输入框:通常 useEffect 足够,除非必须绘制前完成

Q5:动态尺寸图表应该用哪个?

核心思路: 如果图表库对初始尺寸敏感,且错误尺寸会导致明显错位,用 useLayoutEffect;否则优先 useEffect 配合 ResizeObserver。

答案:

分情况:

  1. 图表库必须在首次绘制前拿到正确尺寸

    • 使用 useLayoutEffect
    • 读取容器宽高,同步设置图表尺寸。
    • 避免用户先看到错误尺寸的图表,然后图表跳变。
  2. 图表库支持响应式更新,轻微延迟可接受

    • 使用 useEffect
    • 配合 ResizeObserver 监听容器尺寸变化。
    • 这样不会阻塞首次绘制,性能更好。
  3. 更好的方案

    • 优先使用图表库自带的响应式能力。
    • 使用 CSS 容器查询、ResizeObserverrequestAnimationFrame
    • 避免在 useLayoutEffect 中频繁同步读写布局,导致布局抖动。

Q6:滚动到某个元素附近并添加高亮类名,应该用哪个?

核心思路: 滚动事件本身就是异步交互,轻微延迟可接受,用 useEffect

答案:

使用 useEffect 配合事件监听。

原因:

  • 滚动过程中,用户不会要求高亮必须和滚动像素级同步。
  • useEffect 不阻塞滚动渲染,体验更流畅。
  • 如果使用 useLayoutEffect,每次滚动相关更新都可能阻塞绘制,容易卡顿。

示例:

  • 监听 scrollresize
  • 使用 getBoundingClientRect 判断元素是否接近视口。
  • 更新高亮状态。
  • 组件卸载时移除监听。

Q7:服务端渲染下 useLayoutEffect 有什么问题?

核心思路: 服务端没有真实 DOM,useLayoutEffect 不会执行,并且 React 会警告。

答案:

在服务端渲染中:

  • useLayoutEffect 不能在服务端执行。
  • React 会输出警告,提示 useLayoutEffect does nothing on the server
  • 因为它依赖真实 DOM 布局,而服务端没有浏览器环境。

解决方案:

  1. 如果不需要同步布局,改用 useEffect
  2. 如果必须同时兼容客户端和服务端,封装同构 Hook:
jsx 复制代码
import { useEffect, useLayoutEffect } from 'react';

// 服务端没有 window,因此退化为 useEffect
// 客户端使用 useLayoutEffect,保证绘制前同步执行
export const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

边界:

  • 服务端渲染时不要读取 windowdocument
  • 如果第三方库依赖 DOM,确保只在客户端执行。

Q8:useLayoutEffect 中 setState 会发生什么?

核心思路: 会触发同步重渲染,React 会在浏览器绘制前完成这次更新,因此能避免闪烁,但可能带来性能问题。

答案:

useLayoutEffect 中调用 setState

  1. React 会同步重新渲染组件。
  2. 重新执行 Render 和 Commit。
  3. 浏览器仍然没有绘制。
  4. 最终用户只看到更新后的界面。

好处:

  • 避免用户看到中间状态。
  • 适合测量后立即修正布局。

风险:

  • 如果频繁 setState,可能导致多次同步渲染。
  • 如果逻辑复杂,会阻塞浏览器绘制,造成卡顿。
  • 必须注意依赖数组,避免无限循环。

更好方案:

  • 能在 CSS 层解决的,不要用 JavaScript 测量。
  • 能用 ResizeObserver 异步更新的,优先异步。
  • 只有在确实需要绘制前同步修正时,才在 useLayoutEffectsetState

Q9:useEffect 和 useLayoutEffect 的执行顺序与清理顺序?

核心思路: 同一组件内按声明顺序执行;子组件 Effect 通常先于父组件执行;清理函数先于下一次 Effect 执行。

答案:

  1. 同一组件内

    • 多个 useLayoutEffect 按声明顺序执行。
    • 多个 useEffect 按声明顺序执行。
    • 所有 useLayoutEffect 先于所有 useEffect
  2. 父子组件

    • 通常子组件的 Effect 先于父组件执行。
    • 因为提交阶段是深度优先。
  3. 清理函数

    • 依赖变化或组件卸载时,先执行上一次的清理函数。
    • 然后执行新的 Effect。
    • 卸载时,清理函数用于取消请求、移除监听、清除定时器。

注意:

  • React 严格模式开发环境下,Effect 可能被额外执行一次,用于检测副作用是否纯。
  • 生产环境不会因为严格模式而双调用。

Q10:useInsertionEffect 是什么?和 useLayoutEffect 有什么关系?

核心思路: useInsertionEffect 是给 CSS-in-JS 库使用的,在 useLayoutEffect 之前执行,用于插入样式,避免重新计算布局。

答案:

React 提供了三个与副作用相关的 Hook:

Hook 执行时机 主要用途
useInsertionEffect useLayoutEffect 之前 CSS-in-JS 插入样式
useLayoutEffect DOM 变更后、绘制前,同步 读取布局并同步修改,避免闪烁
useEffect 提交后异步,通常绘制后 大多数副作用

业务代码通常不需要直接使用 useInsertionEffect。它主要供 styled-components、emotion 等库使用。


3. 补充说明

  1. "useEffect 是在浏览器完成布局和绘制之后异步执行"是简化说法

    • 更准确:useEffect 在 React 提交后异步调度执行,通常发生在浏览器绘制后。
    • React 不保证它严格在某一帧绘制后立即执行,但它不会像 useLayoutEffect 那样阻塞绘制。
  2. 遗漏知识点:服务端渲染

    • useLayoutEffect 在服务端渲染会警告。
    • 可用 useEffect 或同构 Hook 替代。
  3. 遗漏知识点:并发模式

    • React 18 并发模式下,useEffect 可能被延迟。
    • useLayoutEffect 仍然同步执行,可能阻塞。
  4. 遗漏知识点:清理函数

    • 请求、订阅、事件监听、定时器都必须清理。
    • 否则会内存泄漏或更新已卸载组件。
  5. 遗漏知识点:依赖数组

    • 空数组:挂载和卸载时执行。
    • 无数组:每次渲染后都执行。
    • 有依赖:依赖变化时执行。
  6. 遗漏知识点:StrictMode

    • 开发环境下 Effect 可能双调用,用于发现副作用问题。
    • 不要依赖 Effect 只执行一次来做不可重复的操作。
  7. 遗漏知识点:useInsertionEffect

    • 用于 CSS-in-JS,在 useLayoutEffect 之前执行。

4. 示例代码

示例 1:useLayoutEffect 测量宽度并同步更新,避免闪烁

jsx 复制代码
import React, { useLayoutEffect, useRef, useState } from 'react';

function AutoWidthBox() {
  // 源元素:用于测量宽度
  const sourceRef = useRef(null);

  // 目标元素宽度:根据源元素宽度同步更新
  const [width, setWidth] = useState(0);

  useLayoutEffect(() => {
    const element = sourceRef.current;

    // 防御:元素可能尚未挂载
    if (!element) {
      return;
    }

    // 读取布局信息
    const updateWidth = () => {
      const rect = element.getBoundingClientRect();
      setWidth(rect.width);
    };

    // 首次同步测量:发生在浏览器绘制前,避免目标元素先显示错误宽度
    updateWidth();

    // 监听后续尺寸变化:ResizeObserver 是异步回调,但初始测量仍是同步的
    const observer = new ResizeObserver(updateWidth);
    observer.observe(element);

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

  return (
    <div>
      <div
        ref={sourceRef}
        style={{
          width: '50%',
          background: '#dddddd',
          padding: '8px',
        }}
      >
        源元素:宽度为父容器的 50%
      </div>

      <div
        style={{
          width: width,
          background: '#ff9999',
          padding: '8px',
        }}
      >
        目标元素:宽度与源元素同步
      </div>
    </div>
  );
}

export default AutoWidthBox;

示例 2:useEffect 请求数据,并处理取消和清理

jsx 复制代码
import React, { useEffect, useState } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 标记是否已取消,防止组件卸载后继续 setState
    let cancelled = false;

    // AbortController 用于取消 fetch 请求
    const controller = new AbortController();

    async function fetchUser() {
      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error('请求用户信息失败');
        }

        const data = await response.json();

        // 只有未取消时才更新状态
        if (!cancelled) {
          setUser(data);
          setError(null);
        }
      } catch (err) {
        // AbortError 是主动取消导致的,不需要展示错误
        if (!cancelled && err.name !== 'AbortError') {
          setError(err);
        }
      }
    }

    fetchUser();

    // 清理函数:依赖变化或组件卸载时执行
    return () => {
      cancelled = true;
      controller.abort();
    };
  }, [userId]);

  if (error) {
    return <div>错误:{error.message}</div>;
  }

  if (!user) {
    return <div>加载中...</div>;
  }

  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

export default UserProfile;

示例 3:useEffect 实现滚动高亮

jsx 复制代码
import React, { useEffect, useState } from 'react';

function ScrollHighlight({ targetId }) {
  const [highlighted, setHighlighted] = useState(false);

  useEffect(() => {
    const target = document.getElementById(targetId);

    if (!target) {
      return;
    }

    const handleScroll = () => {
      const rect = target.getBoundingClientRect();
      const viewportHeight =
        window.innerHeight || document.documentElement.clientHeight;

      // 元素进入视口下方 80% 区域时高亮
      const isNear =
        rect.top < viewportHeight * 0.8 && rect.bottom > 0;

      setHighlighted(isNear);
    };

    // 首次执行一次,避免初始状态错误
    handleScroll();

    // 滚动和尺寸变化时重新计算
    window.addEventListener('scroll', handleScroll, { passive: true });
    window.addEventListener('resize', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
      window.removeEventListener('resize', handleScroll);
    };
  }, [targetId]);

  return (
    <div>
      <div style={{ height: '150vh' }}>滚动区域</div>

      <div
        id={targetId}
        style={{
          padding: '20px',
          background: highlighted ? 'yellow' : 'transparent',
          transition: 'background 0.2s',
        }}
      >
        目标元素
      </div>
    </div>
  );
}

export default ScrollHighlight;

示例 4:服务端渲染下的同构 useLayoutEffect

jsx 复制代码
import { useEffect, useLayoutEffect } from 'react';

// 客户端存在 window,可以使用 useLayoutEffect
// 服务端不存在 window,退化为 useEffect,避免警告
export const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

使用:

jsx 复制代码
import { useRef } from 'react';
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';

function MeasureBox() {
  const ref = useRef(null);

  useIsomorphicLayoutEffect(() => {
    if (!ref.current) {
      return;
    }

    // 客户端会同步执行,服务端不会执行
    const rect = ref.current.getBoundingClientRect();
    console.log(rect.width, rect.height);
  }, []);

  return <div ref={ref}>测量我</div>;
}

5. 满分答案

useEffectuseLayoutEffect 的核心区别是执行时机。useLayoutEffect 在 React 完成 DOM 变更后、浏览器绘制前同步执行,会阻塞浏览器绘制;因此它适合读取布局信息并同步修改界面,避免用户看到闪烁或中间状态。useEffect 在提交后异步执行,通常发生在浏览器绘制后,不阻塞首次绘制;因此它适合数据请求、订阅、事件监听、定时器、日志、滚动高亮等大多数副作用。

主要矛盾是:useLayoutEffect 用同步阻塞换取视觉一致性,useEffect 用异步执行换取流畅性。次要矛盾包括服务端渲染兼容、并发模式调度、性能开销、依赖数组和清理函数。

经验法则是:默认使用 useEffect;只有当 useEffect 导致视觉闪烁、布局跳动、用户看到中间状态,或者必须绘制前同步读取布局并更新时,才使用 useLayoutEffect。典型必须使用 useLayoutEffect 的场景是 Tooltip 定位、Popover 定位、测量 DOM 尺寸并同步设置样式、第三方库需要绘制前初始化。服务端渲染下 useLayoutEffect 会警告,应改用 useEffect 或同构 Hook。React 18 并发模式下,useEffect 可能延迟,useLayoutEffect 仍然同步执行。不要滥用 useLayoutEffect,否则会阻塞绘制,造成性能问题。

相关推荐
ID34610744202 小时前
【课程设计】基于Spring Boot+Vue的校园共享无人机服务系统设计与实现-计算机毕设 附源码44219
javascript·vue.js·spring boot·python·node.js·php·课程设计
夜不会漫长2 小时前
C++:类和对象(2)
java·javascript·c++
掰头战士2 小时前
聊聊 Function Calling,你的模型是否答非所问?
前端·typescript·agent
Yeyu2 小时前
车载多 App 同屏渲染(三):RemoteCompose 序列化 UI
前端
weixin199701080162 小时前
[特殊字符]《京东POP二手品类对接:B2C订单模型 vs 二手C2C属性的字段转换难题》(附Python源码)
前端·python·算法
BillKu2 小时前
Vue3 可拖拽分栏布局:两种实现方式详解
javascript·vue.js·ecmascript
三小河3 小时前
AI 对话 "打字机" 是怎么实现的?从 SSE 流式、Markdown 组件到 Nginx 防粘连
前端·人工智能·面试
雪芽蓝域zzs3 小时前
第三十七节:防抖、节流封装(composables 通用函数,搜索框防重复请求)
前端·javascript·vue.js
by组态3 小时前
Ricon组态系统通信配置指南
前端·后端·物联网