前端开发面试题 React(23 题)

技术 · 2025-02-01
前端开发面试题 React(23 题)

2026-06-26T15:38:46.png

Q1:Fiber 架构的工作原理?

核心要点:Fiber 是 React 16 引入的协调引擎(Reconciliation Engine),将渲染工作拆分成可中断、可恢复的最小工作单元。

工作原理

  1. 数据结构:每个 Fiber 节点对应一个 React 元素,包含 typekeychildsiblingreturnpendingPropsmemoizedPropsmemoizedStatealternate(双缓冲)等字段,形成一棵 Fiber 树。
  2. 双缓冲机制:维护两棵 Fiber 树(currentworkInProgress),更新时构建 workInProgress,完成后整体切换,减少页面闪烁。
  3. 两阶段渲染

    • Render 阶段(可中断):调用 beginWork 向下遍历,构建新 Fiber 树,标记副作用(Placement/Update/Deletion)。
    • Commit 阶段(不可中断):将副作用一次性提交到真实 DOM,调用 useEffect/useLayoutEffect 回调。
  4. 时间切片(Time Slicing):通过 MessageChannelscheduler 包将渲染任务切成 5ms 小片,每帧 requestIdleCallback 思想交还主线程。

简化流程图

scheduleUpdate → scheduleWork → workLoop (yield when no time)
    → beginWork (down) → completeWork (up) → commitWork (sync)

Q2:ReactReconciler 为何要采用 Fiber 架构?

原因总结

  1. 解决同步阻塞问题:React 15 之前采用 Stack Reconciler,递归调用栈一旦开始无法中断,长任务(>16ms)会阻塞主线程,导致卡顿、掉帧。
  2. 支持优先级调度:Fiber 节点可携带 lanes(优先级位),实现高优先级更新(用户输入)打断低优先级更新(数据加载)。
  3. 支持并发特性:为 Concurrent Mode、useTransitionuseDeferredValue 等 React 18 特性提供底层支撑。
  4. 更好的错误恢复:可中断后从断点恢复,结合 Error Boundary 提升健壮性。

Q3:useState 是如何实现的?

实现原理(简化源码):

// 挂载阶段
function mountState(initialState) {
  const hook = mountWorkInProgressHook(); // 创建 hook 节点挂到 fiber.memoizedState 链表
  hook.memoizedState = typeof initialState === 'function' ? initialState() : initialState;
  hook.queue = { pending: null, dispatch: null };
  const dispatch = dispatchAction.bind(null, currentlyRenderingFiber, hook.queue);
  hook.queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}

// 更新阶段
function dispatchAction(fiber, queue, action) {
  const update = { action, next: null, lane: requestUpdateLane() };
  // 链表追加 update
  enqueueRenderPhaseUpdate(queue, update);
  // 调度更新
  scheduleUpdateOnFiber(fiber, update.lane);
}

关键点

  • Hook 以链表形式存储在 Fiber 节点的 memoizedState 上;
  • dispatch 通过调度器触发更新;
  • 多次 setState 在同一事件中会批处理(React 18 自动批处理,Promise/setTimeout 中也合并)。

Q4:React Fiber 是什么?

定义:Fiber 是 React 内部的协调算法 + 数据结构,是对"虚拟 DOM 节点"的升级版。

两个层面的含义

  1. 架构层面:新的协调引擎(Reconciler)。
  2. 数据结构层面:每个 Fiber 节点是一个 JS 对象,包含组件信息、DOM 信息、工作状态、副作用链表等。

核心属性

属性说明
type / elementType组件类型('div' / FunctionComponent / ClassComponent)
keyDiff 时用于识别节点
stateNode真实 DOM / 类实例
return / child / sibling树结构指针
pendingProps / memoizedProps新旧 props
memoizedStateHook 链表 / 类组件 state
flags / subtreeFlags副作用标记(Placement/Update/ChildDeletion)
lanes / childLanes优先级
alternate指向另一棵树的对应节点(双缓冲)

Q5:简单介绍 React 中的 diff 算法

三大策略(将 O(n³) 降为 O(n)):

  1. Tree Diff(树级):同层比较,不跨层级移动节点。若发现节点跨层移动,直接删除重建
  2. Component Diff(组件级)

    • 同类型组件:继续往下 diff;
    • 不同类型组件:直接替换整棵子树。
  3. Element Diff(元素级):通过 key 标识同级元素的唯一性。

    • key:用 key 匹配节点,移动位置而非重建;
    • key:按顺序逐个对比,效率低且易出错。

最佳实践:列表渲染务必提供稳定且唯一的 key(建议用业务 ID,不要用 index)。


Q6:如何让 useEffect 支持 async/await?

直接写法(错误)

useEffect(async () => { // ❌ 返回 Promise,React 不会等待
  await fetchData();
}, []);

正确写法

// 方案1:内部定义 async 函数
useEffect(() => {
  const load = async () => {
    const data = await fetchData();
    setData(data);
  };
  load();
}, []);

// 方案2:抽成函数
const fetchData = async () => { /* ... */ };
useEffect(() => { fetchData(); }, []);

原因useEffect 的回调要么返回 undefined,要么返回清理函数(cleanup)。async 函数返回 Promise,会被当作"清理函数"忽略,导致清理逻辑失效。

可选优化:使用 AbortController 取消请求:

useEffect(() => {
  const ctrl = new AbortController();
  fetch(url, { signal: ctrl.signal }).then(...);
  return () => ctrl.abort();
}, [url]);

Q7:React Fiber 是如何实现更新过程可控?

可控性的三大支柱

  1. 可中断:每次执行一个 Fiber 单元后检查 shouldYield(),时间片用完就 return,把控制权交回调度器。
  2. 可恢复:通过 workInProgress 记住当前进度,下次从断点 Fiber 继续 beginWork
  3. 可优先级:每个更新携带 lane(优先级),高优先级插入队列时可插队抢占低优先级。

实现关键

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

function shouldYield() {
  return performance.now() >= deadline; // 5ms 时间片
}

调度器基于 MessageChannel 实现,比 setTimeout(fn, 0) 更稳定,比 requestIdleCallback 兼容性更好。


Q8:React 中懒加载的实现原理是什么?

核心 APIReact.lazy + Suspense

原理

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <OtherComponent />
    </Suspense>
  );
}
  1. React.lazy 接收返回 Promise 的动态 import()
  2. 首次渲染时 React 抛出 Promise,进入 Suspense 的 fallback;
  3. Promise resolve 后重新渲染,替换为真实组件;
  4. 底层通过 Throw Promise 模式让渲染可中断,等模块加载完成再恢复。

路由级懒加载(React Router v6):

const Home = lazy(() => import('./pages/Home'));
<Route path="/" element={<Suspense fallback={<Loading />}><Home /></Suspense>} />

Q9:React 中怎么实现状态自动保存(KeepAlive)?

场景:列表页 → 详情页 → 返回时保留列表滚动位置和筛选条件。

方案对比

方案实现方式适用场景
缓存组件状态外部 Store(Zustand/Redux)保存状态中小型项目
手动缓存useRef + useImperativeHandle 暴露 reset单组件
第三方库react-activation / react-keep-alive-router完整方案
DOM 节点缓存隐藏的 display: none 容器保留 DOM简单场景

react-activation 示例

import { KeepAlive } from 'react-activation';

<KeepAlive name="list-page" saveScrollPosition>
  <ListPage />
</KeepAlive>

底层通过 Portal 将组件挂载到隐藏容器,路由切换时不卸载。


Q10:React 有哪些性能优化的方法?

按层级

1. 减少渲染次数

  • React.memo / PureComponent 浅比较 props;
  • useMemo / useCallback 缓存引用;
  • 状态下推(拆分组件,缩小影响范围);
  • Context 拆分(细粒度 Context)。

2. 减少渲染计算量

  • 虚拟列表(react-windowreact-virtuoso);
  • 防抖/节流高频事件;
  • Web Worker 处理 CPU 密集任务。

3. 减少包体积

  • 路由懒加载 + 组件懒加载;
  • Tree Shaking;
  • 按需引入第三方库;
  • 压缩图片(WebP/AVIF)。

4. 网络优化

  • CDN、HTTP 缓存、Service Worker;
  • 接口合并、GraphQL;
  • 预加载(<link rel="preload">)。

5. 渲染层

  • SSR/SSG(Next.js);
  • Suspense 流式渲染。

Q11:不同版本的 React 都做过哪些优化?

版本关键优化
React 15Stack Reconciler,diff 算法奠基
React 16Fiber 架构、Error Boundaries、Fragment、Portal、createRef
React 16.3new Context API、React.createRefReact.forwardRef
React 16.6React.lazy + SuspensememoforwardRef
React 16.8Hooks 革命(useState/useEffect/useContext 等)
React 17渐进式升级、新 JSX 运行时(react/jsx-runtime)、事件委托到根容器
React 18Concurrent Renderer、自动批处理、Transitions(useTransition / useDeferredValue)、Suspense SSR 流式、useId、useSyncExternalStore
React 19(实验)Actions、useOptimistic、use() API、ref 作为 prop、<form> Action、<title> 文档元数据

Q12:React 18 新特性

  1. Concurrent Features(并发渲染)

    • createRoot API 启用并发模式;
    • useTransition:标记非紧急更新;
    • useDeferredValue:延迟更新值。
  2. 自动批处理:所有更新(包括 Promise、setTimeout、原生事件处理器)合并渲染。
  3. Suspense 改进

    • 服务端 Suspense 流式 HTML(Streaming SSR);
    • <SuspenseList> 协调多个 Suspense。
  4. 新 HookuseIduseSyncExternalStoreuseInsertionEffect
  5. 严格模式增强:开发模式下组件二次挂载/卸载,复现副作用问题。
  6. Server Components(RSC):服务端组件,零客户端 JS。

Q13:React Hook 的闭包陷阱是什么?解决方案?

陷阱示例

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // 永远打印 0,闭包陷阱
      setCount(count + 1); // 永远 +1(基于 0)
    }, 1000);
    return () => clearInterval(id);
  }, []); // 空依赖,count 永远是最初的 0
}

解决方案

  1. 函数式更新(推荐)

    setCount(c => c + 1); // 总是基于最新值
  2. 使用 ref 保存最新值

    const countRef = useRef(count);
    useEffect(() => { countRef.current = count; }, [count]);
    // 定时器内使用 countRef.current
  3. 正确声明依赖useEffect(..., [count]),但可能造成定时器反复创建/销毁。

根因:每次渲染都会创建一个新的 effect 函数,effect 捕获的是创建时的 props/state。


Q14:React 中,怎么给 children 添加额外的属性?

方案 1:React.cloneElement(最常见)

function MyContainer({ children }) {
  return React.Children.map(children, child =>
    React.cloneElement(child, { extraProp: 'value' })
  );
}

方案 2:Context 透传(更优雅)

const ExtraCtx = createContext({});
function Parent({ children }) {
  const extra = { theme: 'dark' };
  return <ExtraCtx.Provider value={extra}>{children}</ExtraCtx.Provider>;
}

方案 3:HOC / 自定义 Hook 包装

方案 4:React 19 直接 ref 作为 prop,未来属性传递将更自然。


Q15:Fiber 为什么是 React 性能的一个飞跃?

飞跃点

  1. 从同步到异步:Stack Reconciler 必须一次性跑完,Fiber 可中断,让出主线程给高优先级任务。
  2. 从单优先级到多优先级:基于 lane 模型实现 5+ 种优先级,用户输入、动画、数据加载可差异化调度。
  3. 从一次性提交到分阶段:Render/Commit 分离,DOM 变更可控,避免半成品状态。
  4. 为并发特性铺路useTransitionSuspenseServer Components 全部依赖 Fiber。
  5. 降低主线程压力:长列表、大状态树更新不再卡顿,60fps 成为可能。

Q16:React 是否支持给标签设置自定义的属性?

支持,但不推荐

两种方式

  1. HTML 自定义属性:原生支持,data-* 推荐使用,可通过 dataset 访问。
  2. 非标准属性:React 会原样传递到 DOM 上(除了 key/ref/dangerouslySetInnerHTML 等特殊属性)。

    <div custom-attr="hello" data-id="123" />

注意事项

  • 会触发 React 警告 Unknown prop "custom-attr" on <a> tag(如需消除,加 // @ts-ignore 或扩展 JSX 类型);
  • 推荐优先用 data-*,符合 HTML5 规范且语义清晰;
  • 复杂场景建议用 Context 或全局状态管理。

Q17:说说 React render 阶段的执行过程

核心流程

  1. 入口scheduleUpdateOnFiber → 调度器根据 lane 决定立即/延后执行;
  2. 构建 workInProgress 树

    • 复用 current 节点(通过 alternate);
    • 调用 beginWork(current, workInProgress, renderLanes) 处理当前 Fiber;
  3. beginWork 内部

    • 函数组件:执行函数体,收集 Hook;
    • 类组件:调用 render 方法;
    • 调和子节点,标记 Diff(flags);
  4. completeWork:向上回溯,完成 Fiber 节点信息收集,构建 effect 链表;
  5. 循环:在 workLoopConcurrent 中通过 shouldYield() 检查时间片,用完则暂停。

特点:纯 JS 计算,不操作 DOM,可随时中断。


Q18:React 中,Fiber 是如何实现时间切片的?

实现机制

  1. 最小工作单元:单个 Fiber 节点(performUnitOfWork);
  2. 时间片控制

    • 调度器设置 deadline = performance.now() + 5ms
    • 每完成一个 Fiber 调用 shouldYield() 检查是否超时;
    • 超时则退出 workLoop,保留 workInProgress 指针;
  3. 恢复机制:浏览器空闲后(通过 MessageChannel.postMessage)重新进入 workLoop,从上次断点继续;
  4. 底层调度器scheduler 包实现了基于优先级队列和最小堆的调度算法。

为什么不直接用 requestIdleCallback

  • 兼容性差(仅 Chrome 支持);
  • 最小延迟 50ms 太长;
  • 无法精细控制优先级。

Q19:说说 React commit 阶段的执行过程

Commit 阶段三个子阶段

  1. BeforeMutation(DOM 变更前)

    • 调用类组件 getSnapshotBeforeUpdate
    • 调度 useEffect 销毁函数。
  2. Mutation(DOM 变更)

    • 执行所有 DOM 插入/更新/删除;
    • 绑定/解绑 ref;
    • 调用类组件 componentDidMount/componentDidUpdate
  3. Layout(DOM 变更后、浏览器绘制前)

    • 调用 useLayoutEffect 回调;
    • 调度 useEffect 创建函数(异步,避免阻塞绘制)。

关键特性

  • 同步执行,不可中断(保证 DOM 一致性);
  • 整个过程通过 commitRoot 一次性提交 effect 链表。

Q20:React 中的路由懒加载是什么?原理?

概念:按路由拆分代码块,访问对应路由时才加载对应组件的 JS。

实现

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import(/* webpackChunkName: "home" */ './pages/Home'));
const About = lazy(() => import(/* webpackChunkName: "about" */ './pages/About'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<Loading />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

原理

  1. import() 触发 webpack/vite 拆包(自动生成单独 chunk);
  2. React.lazy 把模块包装成懒加载组件;
  3. 首次渲染时 React 抛 Promise,进入 Suspense fallback;
  4. 模块加载完成 resolve 后重新渲染。

优势:首屏 JS 体积显著减少,加快 FCP/TTI。


Q21:React 中为什么不直接使用 requestIdleCallback?

原因

  1. 兼容性问题:仅 Chrome/Firefox 支持,Safari/Edge 早期不支持;
  2. 最大延迟 50ms:对于需要快速响应的场景(动画、输入),延迟过高;
  3. 无法精细优先级requestIdleCallback 只区分 idle 与否,无法满足多优先级调度;
  4. 无法可靠触发:浏览器在标签页非激活时可能完全停止调用,影响数据一致性。

React 的替代方案

  • 使用 MessageChannel 宏任务;
  • scheduler 包实现自定义优先级队列;
  • 通过 shouldYield() + performance.now() 控制时间片。

Q22:React 组件间怎么进行通信?

通信方式汇总

场景方式
父子props / 回调函数
子父回调函数 / ref 暴露实例
兄弟状态提升到共同父组件 / Context
跨层级Context API / Redux / Zustand
任意组件全局状态(Redux / MobX / Zustand / Jotai)
深层组件Composition(children / render props)
命令式useImperativeHandle + forwardRef
外部系统useSyncExternalStore 订阅外部 Store

最佳实践

  • 优先用 props / composition;
  • 跨多层级才考虑 Context;
  • 复杂应用用 Redux Toolkit / Zustand;
  • 避免过度使用 Context(导致不必要的 re-render)。

Q23:React 和 Vue 在技术层面有哪些区别?

维度ReactVue 3
范式函数式 + JSX,强调"UI = f(state)"选项式 + 组合式,渐进式框架
响应式不可变数据 + 显式 setStateProxy 代理 + 自动依赖收集
Diff单端比对双端对比 + 最长递增子序列
编译优化React Compiler(实验)模板编译 + 静态提升 + 补丁标志
Fiber/调度必备不需要(响应式粒度更细)
生态庞大、社区驱动官方全家桶(Router/Pinia/Vite)
学习曲线较陡(Hooks、心智模型)较平缓(模板直观)
TypeScript一流支持良好支持(3.x 改进)
包体积较小核心(~44KB gzip)较小核心(~34KB gzip)
服务端Server Components / Next.jsNuxt 3

本质差异

  • React 假设"一切皆组件",状态更新需要手动 setState;
  • Vue 通过 Proxy 自动追踪依赖,更新粒度更细,不需要 Fiber 也能高效更新。

本文作者:小码哥

本文链接:https://wesee.club/archives/1180/

版权声明:自由转载-非商用-非衍生-保持署名(cc 创意共享 3.0 许可证

Theme Jasmine by Kent Liao

粤ICP备2023052298号-1