openAI
React 面试题库
以下题库按模块分类(React 架构与运行机制、核心 API、Diff 算法与事件机制、Fiber 架构与调度、并发模式与过渡、生命周期及新版生命周期对照、综合源码题、扩展专题、React 与 Vue 对比),并按难度(初级、中级、高级)分组。每题附参考答案,包含原理解析和关键源码思路,并引入文献资料说明。
React 架构总览与运行机制
初级问题
-
问:React 的核心架构由哪些部分组成?
答: React 核心包含两个主要模块:调和器(Reconciler)和渲染器(Renderer)。调和器负责执行组件的render
方法生成虚拟 DOM(Virtual DOM)并对比新旧虚拟 DOM 树以找出差异;渲染器(如 ReactDOM)负责将这些差异应用到真实 DOM 或目标环境中。React 通过虚拟 DOM 实现声明式编程:开发者只需描述 UI 应处于什么状态,React 会负责把真实 DOM 同步到该状态。 -
问:什么是 Virtual DOM?它有什么作用?
答: Virtual DOM 是一种在内存中的虚拟表示,用来模拟真实 DOM。React 在内存中维护虚拟 DOM 树,当组件状态或属性发生变化时,生成新的虚拟 DOM 树并与旧树进行对比(协调/Reconciliation),只将变化部分更新到真实 DOM。这种方式使得开发者可用声明式 API 编写界面,只需指定 UI 应该是什么样子,React 会通过 Virtual DOM 高效更新真实 DOM。 -
问:React Fiber 是什么?为什么引入 Fiber?
答: Fiber 是 React 16 引入的新型协调引擎(即新版本的调和器),其主要目的是支持增量渲染和任务调度。在 React 16 之前,调和过程是递归且同步的,一次更新可能耗时很长,影响浏览器帧率。Fiber 将渲染工作拆分为可中断的任务片段,引入任务优先级调度机制,可以将高优先级任务插队执行,从而让界面保持更好响应性。
中级问题
-
问:React 协调(调和)过程是什么?Diff 算法的核心原则有哪些?
答: 协调(Reconciliation)是 React 同步虚拟 DOM 与真实 DOM 的过程。其核心就是 Virtual DOM 的 diff 算法。React 的 diff 算法主要有三条原则:同级比较(只比较同一层级的节点,不跨层查找)、类型不同则整体替换,类型相同则递归比较(不同类型元素直接替换,类型相同则保留原节点并更新属性,然后对子节点递归比较)、列表节点通过 key 来复用(若列表中节点有唯一key
,React 会根据 key 来复用节点,避免不必要的删除和重建)。这些原则让 diff 过程高效,复杂度从指数级降为线性级。 -
问:React 15 的架构有哪些缺点?React 16 是如何改进的?
答: 在 React 15 中,调和器为栈调和器(Stack Reconciler),即更新过程是同步执行的递归操作。由于浏览器每 16.7ms 重绘一次,如果一次更新递归遍历的组件树很深且复杂,可能导致更新耗时超过一帧,造成页面卡顿。React 16 引入了 Fiber 调和器(Fiber Reconciler)和调度器(Scheduler),将更新拆分为小任务,按优先级调度执行,使得高优先级的任务能打断低优先级的更新,以保持界面流畅。Fiber 在执行中分为两个阶段:渲染(Reconciliation)阶段可以被中断,用于创建新的 Fiber 树并找到变更节点;提交(Commit)阶段一次性将修改同步到真实 DOM,过程不可中断。 -
问:React 如何支持多平台渲染(如浏览器端和原生端)?
答: React 将渲染器(Renderer)抽象为可替换的模块。常见渲染器有ReactDOM
(浏览器 DOM)、React Native
(原生移动端)、React Test
(测试场景下纯 JS 对象)、React ART
(画布、SVG)等。无论哪个渲染器,都复用相同的协调器算法,只需要提供将虚拟 DOM 对象最终转换为目标环境元素的方法。这样 React 通过不同渲染器适配多种平台,同时保留了相同的组件和协调逻辑。
高级问题
-
问:在 React 中,一个组件更新的触发路径是怎样的?
答: 当组件状态或属性改变时,会触发调度更新过程。大致步骤:用户代码调用setState
(类组件)或更新 Hooks 状态,内部会调用调度函数scheduleUpdateOnFiber
;调度器将更新放入更新队列,并启动执行工作循环workLoopSync
。workLoopSync
遍历 Fiber 树:每个工作单元通过performUnitOfWork
调用beginWork
,为当前 Fiber 计算新的输出 Fiber,并将下一个单元推进(深度优先遍历)。完成整棵树后,进入提交阶段(调用commitWork
等),一次性将所有变更应用到真实 DOM。核心源码中涉及函数包括createFiberFromTypeAndProps
(根据元素类型创建 Fiber)和beginWork
、completeWork
等。 -
问:React 更新过程中的优先级调度是如何组织的?
答: React 在协调器中引入了任务优先级的概念,将更新任务分为几个等级:同步(synchronous)、任务(task)、动画(animation)、高(high)、低(low)、离屏(offscreen) 等级。Fiber 调和器将更新拆分为多个小片段执行,每个片段结束后会检查是否有更高优先级任务需要插队。例如,渲染周期内如果有用户输入(高优先)和普通数据刷新(低优先)同时发生,调度器可先中断低优先渲染,优先处理用户输入事件。每种优先级对应不同的调度队列,使得 React 能保持对高优先级更新的快速响应。
React 核心 API(Hooks)
初级问题
-
问:
useState
的基本用法和特性是什么?
答:useState
是函数组件中用于声明状态变量的 Hook。调用const [state, setState] = useState(initialValue)
,返回当前状态state
和更新函数setState
。setState
可以直接传入新值,也可以传入一个接收旧状态并返回新状态的函数(函数式更新)。若传入函数,则会在更新时基于最新状态计算新值。注意:在一次渲染中,直接使用变量更新(setState(state + 1)
)不会累加,因为state
在当前闭包中是固定的;而使用函数式更新(setState(prev => prev + 1)
)会以最新值为基础依次累加。 -
问:
useEffect
有什么作用?默认情况下何时执行?
答:useEffect
用来在函数组件中执行副作用(例如数据获取、DOM 操作、订阅等)。它接收一个副作用函数和可选的依赖数组。默认情况下,副作用函数会在每次组件渲染完成后执行。比如,我们希望组件挂载后更改网页标题,则需要在useEffect
中执行该操作。React 文档说明:每次渲染后执行 effect,如果指定了依赖数组,仅在依赖变化时才执行。在组件卸载时,可返回一个清理函数来撤销副作用(如移除事件监听)。 -
问:
useRef
有什么用途?
答:useRef
返回一个可变的 ref 对象,其current
属性可保存任意值。与useState
不同,修改 ref 的current
不会触发组件重新渲染。常用的用法是获取 DOM 节点引用或在组件渲染之间保持某个值。例如,const inputRef = useRef(null)
后可把<input ref={inputRef}>
绑定到 DOM 元素上,组件渲染后inputRef.current
就指向该 DOM 元素。也可将其用作实例变量,保存不需要参与渲染的值。 -
问:
useMemo
和useCallback
的区别是什么?
答: 两者都是性能优化的 Hook,但用途不同。useMemo
接收一个 “计算函数” 和依赖数组,仅在依赖变化时重新执行函数并返回值,否则返回上一次缓存的值。主要用于避免在每次渲染时做昂贵的计算。useCallback
接收一个函数和依赖数组,只有在依赖变化时才返回一个新的函数实例,否则返回上一次的同一个函数。因此,useMemo
缓存的是计算结果值,useCallback
缓存的是函数本身。底层原理中两者实现机制相同,都是通过比对依赖项决定是否更新。
中级问题
-
问:
useState
的更新是同步还是异步?多次调用有何区别?
答: 在 React 中,函数组件内调用setState
并不会立即更新state
变量,它是异步批量调度的。这意味着在同一个渲染周期里直接使用旧的state
计算新值会无效。例如连续三次setCount(count + 1)
只会导致最终值加 1;而如果使用函数式更新setCount(prev => prev + 1)
三次,则最终加了 3。这是因为前者三次执行时闭包中的count
不变,而后者通过函数参数拿到最新状态逐次累加。在需要基于先前状态连续更新时,应使用函数式更新形式。 -
问:
useEffect
与useLayoutEffect
有何区别?
答: 两者 API 相同,唯一区别是执行时机不同。useEffect
的副作用在浏览器完成画面更新后异步执行,适用于一般的异步操作(数据请求、订阅等)。而useLayoutEffect
的副作用会在 DOM 变更并渲染到屏幕之前同步执行,常用于需要同步测量 DOM 或做防闪烁处理的场景。简单说,useLayoutEffect
在浏览器绘制前执行,useEffect
在绘制后执行,以免阻塞渲染。若无特殊需求,优先使用useEffect
。 -
问:
useCallback
和useMemo
底层原理一致吗?有什么区别?
答: 在 React 18 源码中,useCallback
与useMemo
的内部实现是相同的:都调用了名为areHookInputsEqual
的依赖项比较函数。当依赖未改变时,两者都会返回缓存内容;唯一区别在于:useMemo
返回新计算的值对象,useCallback
返回新创建的函数实例。因此,两者实际上都是在对比依赖数组后决定是否更新缓存,只是缓存内容不同,一个缓存数据,一个缓存函数。
高级问题
-
问:React Hooks 的更新队列是如何管理状态更新的?
答: 在 React 源码中,每个函数组件对应的 Fiber 节点上,会维护一个 Hook 链表,每次useState
调用都会往链表上挂钩子(Hook)对象。调用setState
时,React 会将更新(Update 对象)放入对应 Hook 的更新队列,并通过调度函数scheduleUpdateOnFiber
触发 Fiber 树的重新协调。在下次渲染时,React 会遍历该组件的 Hook 链表,依次取出每个 Hook 的更新队列,按顺序应用各个更新(队列采用环形结构,每次重置)。这样,组件会基于先前状态逐步计算出最新状态并渲染。 -
问:
useState
、useEffect
等 Hooks 的调用顺序有什么要求?为什么?
答: React 要求 Hooks 在组件顶层按照固定顺序调用,不能放在条件、循环或子函数中。这是因为 React 通过调用顺序将每个 Hook 对应到 Fiber 节点上的 Hook 存储结构:每次渲染都会按调用顺序依次“消费”对应的 Hook 缓存位置。如果调用顺序不一致,就会导致前后渲染时 Hook 对应错乱,状态错位。因此所有 Hook 必须在组件体顶部或自定义 Hook 内部,并且条件分支中要保持所有分支调用相同数量的 Hooks。
React Diff 算法与事件机制
初级问题
-
问:React 的 Diff 算法有哪些重要特性?
答: React 的 Diff 算法主要针对同层级节点进行比较,对于跨层级的节点差异采用整体替换的策略。其核心思想可以归纳为三点:一是同层比较——不同层级的节点不做跨层调整,同层内如果新节点不存在则删除对应旧节点;二是类型匹配——如果元素类型不同,则直接销毁旧节点并创建新节点;若类型相同(同为标签或同为组件),则仅更新属性和递归比较子节点;三是列表 Key——在可重排列表的更新中,如果给列表元素加上唯一 key,React 会根据 key 来复用节点,避免无谓的 DOM 删除和重建。这三原则保证了更新效率并尽量减少实际 DOM 操作。 -
问:什么是 React 的合成事件(SyntheticEvent)?它的作用是什么?
答: 合成事件是 React 对浏览器原生事件的跨浏览器封装。React 所有事件处理函数接收到的参数都是一个合成事件对象,它模拟了标准的 DOM 事件接口,提供一致的 API。在内部,React 采用事件委托的方式,只在根容器(React 17+ 中为对应根节点)统一注册少量事件监听器,当事件触发时通过合成事件对象统一派发给组件中的回调。使用合成事件的好处是:对不同浏览器原生事件有一致的处理方式,还可以实现如事件池机制(回收对象提高性能)等优化。合成事件的属性和方法与原生事件类似,例如event.target
、event.preventDefault()
等都是可用的。 -
问:React 中如何阻止事件冒泡?
答: 在 React 合成事件中,可以通过调用event.stopPropagation()
来阻止事件传播(冒泡)。需要注意的是,React 的合成事件会先触发 React 层的捕获/冒泡回调,再触发原生 DOM 的事件传播。调用stopPropagation()
不会阻止 React 内部的合成事件生命周期,但会阻止原生事件的继续传播。此外,也可以在JSX中直接指定onClick={e => e.stopPropagation()}
来防止向上传递。
中级问题
-
问:解释 React 事件委托的机制。
答: React 使用事件委托技术来处理事件。早期版本(React 17 之前)将所有事件绑定到document
上,由 React 自己内部判断目标并派发到对应组件;React 17+ 将事件绑定到各个根 DOM 节点上,但仍是统一管理。这样做的优点是减少了浏览器事件监听器的数量,方便统一处理。对于不能冒泡的事件(如onScroll
),React 会直接在对应元素上绑定原生监听器。因此在 React 中,不需要手动为每个元素都添加事件监听函数,React 在后台自动做了委托。 -
问:React Diff 算法的复杂度是多少?如何通过 key 优化列表渲染?
答: 默认情况下,React 的 diff 算法在比较同层子节点时,如果没有 key,采用索引对比:新旧节点按位置一一比较,同层比较的复杂度是 O(n)。如果指定了稳定的 key,React 会基于 key 构建映射表,匹配更效率更高,以避免大量节点移动。也就是说,给列表元素设置唯一且稳定的key
可以极大提升列表更新效率,因为 React 会复用具有相同 key 的组件和 DOM,减少不必要的删除和重建,且更加准确地保持组件状态。 -
问:在 React 中合成事件对象何时被回收?有什么注意事项?
答: 早期版本的 React 使用事件池来回收合成事件对象,以降低频繁创建对象的开销。也就是说,React 在派发完事件后,会将合成事件对象的属性重置并放入池中重复使用。这意味着如果在异步回调中访问事件对象(如在setTimeout
后),可能会遇到事件已被清空的问题。如果要在异步中使用事件,可以调用event.persist()
来将其从池中移除。需要注意的是,React 17+ 已经移除了事件池,合成事件现在不会自动重用,所以可直接在异步中访问事件属性也安全。
高级问题
-
问:描述一次 React 组件更新时的事件处理流程。
答: 当组件触发事件(如按钮点击)时,React 首先在根节点捕获到原生事件,然后创建一个合成事件对象(SyntheticEvent)。React 会按照捕获/冒泡阶段在虚拟 DOM 树上从上往下或从下往上查找符合触发条件的组件,并依次调用其事件处理函数。事件处理函数执行期间可以调用event.preventDefault()
、event.stopPropagation()
等方法。事件调用结束后,如果事件对象是可复用的,React 会将其属性清空并回收到对象池。这一切都在更新组件状态或执行副作用前完成,之后如有状态更新会触发组件重新渲染。 -
问:React 的
unstable_flushDiscreteUpdates
有什么作用?
答: (源码级问题)unstable_flushDiscreteUpdates
是 React 内部用于同步处理离散事件(如点击、输入等)的低层 API。在并发模式下,用户交互事件会优先级更高,而非紧急更新。使用flushDiscreteUpdates
可以强制立即同步地刷新这些离散更新,保证在用户交互中优先响应。这是 React 调度器内部处理任务优先级的一部分,通常在 React 内部自动调用,用户代码一般无需直接使用。其核心原理在于将某些更新标记为同步更新,从而绕过时间切片调度立即执行。
Fiber 架构与调度机制
初级问题
-
问:React Fiber 架构带来了哪些变化?
答: Fiber 架构引入了**可中断的协程(协作式调和)**机制。在旧的架构中,更新是同步且不能打断的;Fiber 将每次更新过程拆分为多个小任务(Fiber 单元),并在两个渲染周期之间检查是否有更高优先级的任务需要插入。Fiber 架构将组件树的每个节点用 Fiber 对象表示,每个 Fiber 节点包含必要的状态和副作用信息,并通过链表方式链接。Fiber 架构允许任务优先级调度,使得高优先级任务可以中断低优先级任务,从而提升界面响应性。 -
问:React 调度器的“渲染阶段(render)”和“提交阶段(commit)”有何区别?
答: 在 Fiber 架构中更新分为两大阶段:渲染(协调)阶段和提交阶段。渲染阶段会生成新的 Fiber 树,并找出需要更新的节点,这一过程可以被高优先级任务打断和暂停。提交阶段则一次性将渲染阶段收集的所有变更应用到实际 DOM,这是一个不可中断的过程。简单来说,渲染阶段是构建新 UI 的步骤,提交阶段是落地更新的步骤。渲染阶段还会触发新旧状态/属性对比和生命周期(新 API)调用,而提交阶段在更新 DOM 后调用componentDidMount
/componentDidUpdate
等效果钩子。 -
问:简述 React 调度器如何处理任务优先级?
答: React 调度器为不同类型的更新任务分配不同的优先级等级,比如同步、用户阻塞、普通、闲置等(具体名称和数量随版本变化)。当有多个任务排队时,React 调度器会先执行优先级高的任务(如用户输入),高优先级任务可能打断正在进行的低优先级渲染任务。每个任务执行一小段后都会检查是否有更高优先级任务需要处理,以决定是否继续当前任务或暂缓。通过这种策略,React 能确保关键渲染(如及时响应用户交互)优先完成,而不那么紧急的更新则延后执行,提高了应用响应速度。
中级问题
-
问:Fiber 树中的 alternate 属性有什么作用?
答: 在 Fiber 架构中,每个组件实例在任意时刻最多有两个 Fiber 对象:一个是当前渲染完成的 Fiber(current fiber),另一个是正在构建中的 Fiber(work-in-progress fiber)。这两个 Fiber 互相通过alternate
属性指向对方。当一次更新开始时,React 会在已完成 Fiber 树的基础上克隆出一个工作 Fiber 树(work-in-progress),所有状态更新都写在克隆的树上。构建完成后,工作 Fiber 树替换为当前 Fiber 树。这种双树机制使得更新变得“可撤销”:只有当整个工作 Fiber 树准备就绪且无错误时才会提交。 -
问:React 调度器在低优先级任务时会怎样处理?
答: 对于低优先级任务,React 会尽可能拆分成更多的小任务并使用时间切片(Time Slicing)机制。它会检查浏览器剩余可用时间,如果时间耗尽就暂停当前任务,等下一次机会再继续。如果在拆分过程中出现了优先级更高的任务(例如用户输入事件),React 会立即暂停低优先级任务,转去处理高优先级任务。这样可以保证浏览器主线程不会被长时间占用,从而避免界面卡顿。 -
问:React 的批处理(batching)机制是如何实现的?
答: React 在事件处理和生命周期回调中默认启用了状态更新的批处理:多个setState
调用会合并成一次更新以减少渲染。底层原理是 React 使用队列来收集同一事件循环中的所有更新,待事件结束后再统一执行协调。对于用户自定义的原生事件或异步调用(如setTimeout
),React 18 开始默认也会对多次更新进行批处理(通过flushSync
手动绕过除外)。批处理通过调度器统一管理更新队列,保证多次更新只触发一次协调过程,提高性能并避免重复渲染。
高级问题
-
问:在源码级别,React 更新时的工作循环函数主要有哪些?
答: 在 React 源码中,调度更新的核心流程涉及多个函数。简要流程:React 启动调度后调用workLoopSync()
(同步模式)或workLoopConcurrent()
(并发模式)。这会循环执行单元任务,每个任务由performUnitOfWork()
处理,它内部会调用beginWork()
来开始对当前 Fiber 的处理。beginWork
会根据 Fiber 类型生成子 Fiber,并返回下一个待处理的子 Fiber;performUnitOfWork
收尾后会调用completeUnitOfWork()
。循环直到 Fiber 树遍历完成,随后进入提交阶段。可见,关键函数有:createFiberFromTypeAndProps
(创建 Fiber)、workLoopSync
、performUnitOfWork
、beginWork
、completeWork
和commitWork
等。 -
问:React Scheduler 中的时间切片是如何检测何时暂停的?
答: 在并发模式下,React Scheduler 会定期检查剩余的浏览器时间。如果时间片用完,即超过阈值(如 5ms),它会让出控制权给浏览器。源码中,通过类似shouldYieldToRenderer()
的检查来决定是否暂停当前工作单元。若检测到有更高优先级任务或时间不足,则 React 会暂停当前 Fiber 树的构建,保存工作进度,稍后再恢复。这种策略确保 React 不会长时间占用主线程,维持页面流畅。开发者在startTransition
等并发特性中可配置超时时间(timeout),控制切换前最大等待时间。
并发模式(Concurrent Mode)与过渡(Transition)
初级问题
-
问:什么是 Concurrent Mode?React 18 中如何开启?
答: 并发模式是 React 18 引入的一系列实验性渲染特性,总体目标是让 React 可以更灵活地调度渲染任务,从而提供更流畅的用户体验。它允许 React 在执行更新时可中断、可恢复,处理多个更新任务时给予高优先级更新先行机会。要启用并发模式,需要使用ReactDOM.createRoot
而非ReactDOM.render
来创建根节点。例如:ReactDOM.createRoot(rootElement).render(<App />)
。启用后,React 会在新根上启用并发能力,包括startTransition
等特性。 -
问:React 18 中
startTransition
和useTransition
有什么作用?
答: 在并发模式下,React 提供了startTransition
和useTransition
用于标记过渡更新(transition)。这些工具使开发者能够将某些更新标记为“非紧急”的过渡更新,React 会把它们放入低优先级队列。举例来说,当点击切换页面按钮时,我们可以用startTransition
包裹实际数据加载状态的更新,以便在内容加载完成之前先保持旧界面不变。useTransition
返回[startTransition, isPending]
:startTransition(fn)
用于标记更新,isPending
表示过渡是否进行中。当使用了过渡更新,如果更新耗时超过了设置的超时时间(如timeoutMs
),React 会显示 loading 指示,同时在后台等待数据加载完毕再切换页面。总体作用是让页面在执行大更新(如加载新页面)时保持响应性,而不会直接闪现空白。 -
问:
useDeferredValue
与useTransition
的区别是什么?
答: 两者都是并发模式下的优化工具,但用途不同。useTransition
用于将整个状态更新标记为过渡任务,即整个更新的优先级降低;而useDeferredValue
用于“延迟”某个值,它会返回一个缓慢更新的版本。例如,当输入框实时更新列表过滤时,可以用useDeferredValue
返回过滤值的延迟版本,在该值变化时先显示旧列表,等待新的过滤结果准备好再更新。通俗地说,useTransition
是标记更新任务,useDeferredValue
是标记某个值为延迟。两者都依赖并发模式,但场景不同:前者常用于大规模界面更新(导航、数据加载),后者用于节流高频更新的值。
中级问题
-
问:给出一个使用
useTransition
的示例,并解释其作用。
答: 示例:在并发模式下,点击 “Next” 按钮切换用户 ID:import React, { useState, useTransition } from 'react';function App() {const [resource, setResource] = useState(fetchProfileData(initialId));const [startTransition, isPending] = useTransition({ timeoutMs: 3000 });const handleClick = () => {startTransition(() => {const nextId = getNextId(resource.userId);setResource(fetchProfileData(nextId));});};return (<><button onClick={handleClick} disabled={isPending}>Next</button>{isPending && <p>Loading...</p>}<Profile resource={resource} /></>); }
在这个例子中,我们将状态更新包裹在
startTransition
中,这告诉 React 这是一个可以延迟的过渡更新。点击 “Next” 时,如果新页面需要较长时间加载(超过 3000ms),React 会先继续显示旧页面并在一段时间后(超时后)自动切换到新页面。isPending
用于指示过渡是否进行中,此时可以显示加载指示符。这样用户不会看到短暂的空白页,而是更流畅的过渡效果。 -
问:在并发模式下,React 如何处理数据获取(Suspense)?
答: 并发模式下,React 对数据获取和异步操作提供了 Suspense 机制。组件可以使用<Suspense>
包裹异步加载的部分,当内部发生数据加载(或组件惰性加载)时,React 会显示回退 UI(fallback),而非整个页面挂起。结合并发模式,React 可以同时渲染多个状态下的 UI。例如,在页面切换时,我们可以通过将包含数据获取的组件放入 Suspense 中,实现当新数据未就绪时,先展示旧内容或 loading 指示,提升用户体验。React18 进一步提供了startTransition
和 Suspense 的结合,使得复杂场景下界面切换更自然。简单说,Suspense 让数据获取可暂停并显示优雅的占位界面,而并发模式让整个过程更可控。 -
问:如何让 React Server Components 与并发模式一起工作?
答: React 服务器组件(RSC)与并发模式结合使用时,可以在服务器端预渲染部分组件并发送给客户端,客户端在并发模式中继续渲染剩余组件。在使用并发模式的应用中,可以将部分无需客户端交互的界面逻辑标记为服务器组件,它们会在构建或请求时在服务器执行(例如获取数据、生成静态内容)。客户端接收到服务器组件输出后,将其作为静态内容合并到页面中,其间可以使用<Suspense>
等并发特性来处理异步加载的客户端组件。具体实现依赖构建工具,但核心思想是“服务端完成它擅长的工作,将少量数据/渲染结果传给浏览器,浏览器负责剩余的交互渲染”。
高级问题
-
问:并发模式和过渡机制的底层调度逻辑是怎样的?
答: 在并发模式下,React 调度器通过 Fiber 架构将更新任务分片,并维护多个优先级队列。startTransition
将更新标记为低优先级任务。调度器在每次工作循环后检查是否有更高优先级任务并中断当前任务。对于过渡更新,React 会在后台执行,并依据timeoutMs
参数决定显示加载指示的时机。具体来说,startTransition
内部会调用setState
,该更新被放入低优先级队列;然后 React 调用渲染任务时,会允许其他高优先级事件先行。若超过超时阈值,还未完成过渡更新,React 会把过渡更新视为失败,立即渲染 fallback UI,并继续后续更新。完成后,新内容会自动取代旧内容。这样底层调度结合时间切片、任务优先级和等待超时,确保过渡体验顺滑且响应迅速。 -
问:如何在并发模式下测试过渡和悬挂(Suspense)功能?
答: 测试并发功能需要在测试环境启用并发根容器。例如使用 React 测试库可以调用createRoot
而非render
创建组件并挂载。对于 Suspense,可以模拟网络延迟并断言等待状态和最终状态。对于useTransition
,可以在测试中触发startTransition
包裹的更新,并通过isPending
判断过渡状态。需要注意,由于并发行为和时间切片可能导致渲染顺序非直观,测试可能要使用act()
等方法控制调度。官方文档和社区示例中提供了并发模式下测试的指导(如等待下一个微任务或手动触发时间片结束),这里不做赘述,但关键是把握好异步控制。
生命周期与新版生命周期函数对照
初级问题
-
问:React 类组件的生命周期有哪些阶段?
答: React 类组件生命周期大致可分为挂载(Mount)、更新(Update)和卸载(Unmount)三个阶段。挂载阶段的常用方法包括constructor
、static getDerivedStateFromProps
、render
、componentDidMount
。更新阶段有static getDerivedStateFromProps
、shouldComponentUpdate
、render
、getSnapshotBeforeUpdate
、componentDidUpdate
。卸载阶段对应方法是componentWillUnmount
。需要注意的是,React 16.3 之后引入了getDerivedStateFromProps
和getSnapshotBeforeUpdate
,用来替代部分旧的生命周期方法(参见下文)。 -
问:React 中有哪些新的生命周期方法?旧方法为什么弃用?
答: React 16.3 之后,引入了两个静态生命周期方法:static getDerivedStateFromProps(props, state)
和getSnapshotBeforeUpdate(prevProps, prevState)
。前者在每次渲染前(挂载和更新时)被调用,用于根据新的 props 更新 state;后者在更新阶段的render
之后、DOM 提交之前执行,可用于读取更新前的 DOM 状态(如滚动位置)。原先的三个 “Will” 方法(componentWillMount
、componentWillReceiveProps
、componentWillUpdate
)因为在并发模式和未来版本中会被多次调用而被弃用,被标记为UNSAFE_
前缀版本。React 官方建议尽量使用新的生命周期来避免潜在问题。 -
问:说明
componentDidMount
和useEffect
的关系。
答:componentDidMount
是类组件在挂载完成后执行的钩子,用于启动数据请求、订阅等副作用操作。等价地,在函数组件中用useEffect
(不带依赖数组或依赖为空数组)即可模拟同样的行为。例如,useEffect(() => { /* AJAX请求 */ }, [])
在组件首次渲染后执行,就相当于componentDidMount
。区别是函数组件的 Effect 可以在卸载时返回清理函数,而类组件需在componentWillUnmount
中做清理。需要注意,useEffect
在开发模式下有时会被调用两次(StrictMode),但只要编写幂等的副作用函数即可。
中级问题
-
问:Vue 与 React 的生命周期有哪些异同?
答: 相似点: 两者都提供在组件不同阶段(创建、挂载、更新、销毁)执行代码的机会,都可以在组件挂载或更新完成后执行异步操作。
不同点: Vue 和 React 生命周期钩子不完全相同。Vue 提供更多细致的阶段,如beforeCreate
、created
、beforeMount
、mounted
、beforeUpdate
、updated
、beforeDestroy
、destroyed
等;而 React 主要有挂载、更新、卸载等阶段,每阶段以较少的方法覆盖,且新版中使用静态方法替代了部分旧钩子。此外,Vue 的生命周期设计强调数据响应式与模板更新的配合,React 的生命周期更多与虚拟 DOM 协调过程和状态驱动渲染相关。总体来说,Vue 的生命周期更倾向于响应式更新前后的细粒度控制,React 则侧重于函数执行和 DOM 提交的过程。 -
问:React 16.3+ 之后哪些生命周期发生了变化?
答: 在 React 16.3 及之后的版本中,以下变化比较重要:- 旧的
componentWillReceiveProps
被static getDerivedStateFromProps
替代,用于派发新的 props 到 state。 - 旧的
componentWillMount
和componentWillUpdate
被getSnapshotBeforeUpdate
部分替代,该方法用于在更新前从 DOM 获取信息。旧的componentWill*
方法被标记为UNSAFE_
,不推荐使用。 - React 新增了错误边界的生命周期
getDerivedStateFromError
和componentDidCatch
(分别静态和普通方法)来处理渲染错误。
总之,新版生命周期强调“同步计算新状态”和“读取更新前快照”,去掉了可能被多次调用的旧钩子,减少了副作用的不确定性。
- 旧的
-
问:比较 React 和 Vue 的组件通信方式。
答: React: 主流方式是通过 props 从父组件向子组件传递数据,以及通过回调函数(将函数作为 prop)让子组件向父组件发送事件。另有Context
提供跨层级组件共享数据的能力。Vue: 支持类似的 props 传值机制,同时通过自定义事件(组件内使用$emit
触发,父组件通过v-on
监听)实现子到父的通信。此外,Vue 特有的v-model
语法提供了双向绑定支持。总体而言,两者都强调单向数据流,但 Vue 对双向绑定(如表单v-model
)有内置支持,而 React 中双向需要手动管理。
高级问题
-
问:React 16.3+ 废弃了哪些生命周期?为什么?
答: React 16.3+ 废弃(标记为 UNSAFE)了以下生命周期:componentWillMount
、componentWillReceiveProps
、componentWillUpdate
。废弃原因是它们会在并发模式下可能被多次调用,容易导致副作用产生不确定的结果。React 官方将其替换为更安全的静态方法:getDerivedStateFromProps
可替代componentWillReceiveProps
,getSnapshotBeforeUpdate
部分替代componentWillUpdate
。通过这两个方法,React 保证了生命周期方法在执行时更可预测,并避免在渲染阶段产生副作用。 -
问:React 生命周期函数中哪些可能被多次调用,为什么要避免在其中执行副作用?
答: 在 Fiber 协调阶段中,旧版生命周期(如componentWillMount
、componentWillUpdate
、componentWillReceiveProps
)在某些情形下可能被调用多次(例如在中断后重新尝试渲染时)。因此,React 文档提示避免在这些方法里执行一次性副作用(如 AJAX 请求)。这也是 React 16.3+ 废弃这些方法并引入安全替代的原因之一。在新的推荐方法(getDerivedStateFromProps
等)中,也不应放置副作用逻辑。组件挂载完成后应把副作用放在componentDidMount
或useEffect
中。 -
问:如何在 React 中实现类似 Vue
beforeDestroy
的功能?
答: 在类组件中,可以在componentWillUnmount
生命周期中执行卸载前的清理逻辑(类似 Vue 的beforeDestroy
和destroyed
)。在函数组件中,对应的钩子是useEffect
返回的清理函数:当组件卸载时,React 会执行该清理函数。例如:useEffect(() => {// 副作用逻辑return () => {// 卸载时执行的清理逻辑}; }, []);
这样可以在组件销毁前清理定时器、取消订阅等操作,与 Vue 的卸载钩子作用相同。
综合面试题(源码层面)
-
问:当调用
setState
时,React 内部是如何调度更新的?
答: 在类组件里,setState
会调用enqueueSetState
将状态更新包裹为 Update 对象插入更新队列。然后通过调度函数scheduleUpdateOnFiber
将包含该组件的 Fiber 标记为需要更新。调度函数确定更新优先级并安排渲染。随后,React 将在合适时机遍历 Fiber 树,重新执行渲染(render
)方法,构建新的 Fiber 树。最后进入提交阶段将变更应用到 DOM。以上过程包括函数调用栈scheduleUpdateOnFiber
->performSyncWorkOnRoot
->workLoopSync
->performUnitOfWork
等,具体可以在 React 源码中追踪。 -
问:React 是如何保证函数组件
useState
的更新是基于上一次最新状态的?
答: 当使用函数式更新(setState(prev => newValue)
)时,React 在更新时会读取对应 Hook 的上一次状态并传给回调函数。例如,连续多次调用setCount(c => c + 1)
时,React 会依次执行回调,每次prev
都是上一次更新后的最新值。这是因为 Hooks 的更新队列按顺序处理,不断将上一次的状态传给回调。在 Fiber 架构下,Hook 的状态保存在 Fiber 的memoizedState
链表中,每次更新都会取链表尾部最新的 state 传给下一个更新。 -
问:解释 React 中 key 的作用以及如何正确使用。
答:key
是 React 用于区分同层级元素的标识符,对列表渲染尤为重要。如果列表有唯一的key
,React 在 diff 时会将新旧节点按照 key 匹配,并复用不变的节点,从而只更新发生变化的部分。正确使用时,key 应该是能够唯一标识元素且在变动中稳定的值(例如数据项的唯一 ID),不要使用数组索引作为 key(因为重新排序会导致所有元素的 key 发生变化)。这样可以避免不必要的节点删除与重建,保持组件状态和内部 DOM 的稳定。 -
问:在服务端渲染(SSR)与客户端渲染中,React 生命周期有何不同?
答: 在 SSR(服务器渲染)时,React 只执行“挂载”过程的部分内容生成 HTML,但不会执行任何副作用生命周期,如componentDidMount
、useEffect
等,这些只会在客户端挂载时执行。在服务器端执行的只是renderToString
等方法生成 HTML 标记。换言之,服务器端渲染不会触发真实 DOM 操作相关的生命周期,开发者应避免在挂载时直接访问document
或做客户端-only 的初始化。在客户端重新挂载(hydrate)后,才会触发正常的挂载后钩子。
扩展专题
-
问:简述 React Hooks 的底层原理(例如
useState
)。
答: React Hooks 通过在函数组件对应的 Fiber 节点上构建一个 Hook 链表来管理状态。每次渲染,React 会沿着 Fiber 的memoizedState
链表“消费”每个useState
、useEffect
等 Hook,维护一个单向链表保存所有 Hook 对象。useState
返回的setState
函数实际上会将更新加入当前 Hook 的更新队列,并通过调度触发 Fiber 更新。Fiber 在执行时,会遍历这些 Hook 并更新状态值。由于 Hook 的调用顺序必须固定,React 才能正确匹配更新后的状态值与相应的 Hook。在源码层面,Hooks 依赖于内部的调度器(Dispatcher)机制和 Fiber 上保存的memoizedState
指针,确保每次渲染时状态的一致性。 -
问:什么是 React Server Components(RSC)?其工作原理是怎样的?
答: React 服务器组件是一种新型组件概念,允许在服务器端渲染组件并将结果发送给客户端。服务器组件在构建时或请求时在服务器环境中执行,可以执行 I/O 操作(读取文件、请求数据库等),并将生成的内容作为序列化数据(例如 JSON 或 HTML)传递给客户端。客户端接收到服务器组件输出后,会将其结果(及必要的组件元数据)作为道具传递给客户端组件,继续完成界面的渲染。通过将数据获取和静态渲染责任放在服务器,可以减少客户端需要下载和执行的 JavaScript 代码量,提高性能。简言之,服务器组件让服务器先做好部分渲染工作,然后客户端接管剩余工作。 -
问:Redux 和 Recoil 有何区别?各自的设计理念是什么?
答: Redux 和 Recoil 都是状态管理库,但设计理念不同。Redux 采用单向数据流,将所有应用状态集中在一个全局 Store 中,通过 dispatch Actions 和 Reducers 进行管理。它强调不可变状态和显式的更新流程,并且依赖中间件(如 thunk、saga)处理异步逻辑。Recoil 则借鉴了 React Hooks 思想,使用“原子”(atom)作为最小状态单元,每个 atom 代表一个可独立订阅的数据片段。组件可以在任意位置订阅原子,状态更局部化。Recoil 内置了响应式机制(selector)可以自动追踪依赖。简而言之,Redux 适合大型应用的集中式状态管理和严格数据流控制,Recoil 则更贴合 React,支持局部状态和自动优化更新。 -
问:比较 React Context 与 Redux,什么时候使用哪种方案?
答: React Context 主要用于通过组件树传递少量全局数据(如主题、国际化设置等),它提供了类似 “全局变量” 的功能,但并非专门为状态管理设计。Context 更新会导致所有订阅该 Context 的组件重新渲染,适用于全局少量状态。Redux 则是一套完整的状态管理方案,适合复杂应用。它提供了集中化的 Store、可追踪的更新日志(DevTools)、中间件、严格的更新流程等。通常,当需要管理的状态量较大、需要多人协作或需要更精细的调试与优化时,选择 Redux;若只需简单地在应用不同层级共享少量状态,Context 就足够了。总之,Context 更轻量而专注于跨层传递,而 Redux 更擅长复杂场景的状态管理。
React 与 Vue 对比专题
-
问:React 和 Vue 在响应式原理上有哪些差异?
答: 虽然两者都采用虚拟 DOM 来优化渲染,但它们的数据响应机制不同。React 的核心是单向数据流:组件通过 props 接收数据,状态更新后手动调用setState
/Hooks 来重新渲染组件。React 本身不自动跟踪依赖,开发者需要明确设置 state。Vue 则采用双向数据绑定和响应式系统:Vue 2 用Object.defineProperty
,Vue 3 用Proxy
监听数据变化,当数据改变时自动通知相关组件更新。因此,Vue 的模板数据和视图绑定非常紧密,而 React 更强调由开发者显式控制更新。 -
问:React 和 Vue 的生命周期钩子有哪些不同?
答: Vue 的生命周期钩子更为细致,从实例创建到销毁有多个阶段:如beforeCreate
/created
(实例创建)、beforeMount
/mounted
(挂载)、beforeUpdate
/updated
(更新)、beforeDestroy
/destroyed
(卸载)等。React 的类组件生命周期相对简洁:主要是挂载(constructor
、componentDidMount
)、更新(shouldComponentUpdate
、componentDidUpdate
)和卸载(componentWillUnmount
)三个阶段,并辅以静态方法getDerivedStateFromProps
和getSnapshotBeforeUpdate
。Vue 的钩子与数据响应紧密结合,可直接访问实例上下文,React 的钩子则与虚拟 DOM 协调过程结合更多。总体上,Vue 提供了更多粒度的钩子,React 的钩子设计更关注性能和可控性。 -
问:比较 React 和 Vue 的组件通信机制。
答: 在组件通信上,两者有相似点也有差异。相似点: 都支持通过 props 自顶向下传递数据,通过事件或回调自下而上传递消息。
不同点: Vue 内置了更直观的通信方式,如$emit
触发自定义事件,以及v-model
双向绑定简化父子组件数据同步。Vue 还可以通过$refs
或全局事件总线等方式通信。React 社区常用的方式是:父组件将处理函数通过 props 传给子组件,子组件通过调用该函数传递数据;也可使用 Context、Redux 等管理跨层数据。Vue 的事件系统在模板层面上更便捷,而 React 更依赖代码层面的模式(prop drilling 或 Context)。 -
问:在性能优化方面,React 和 Vue 有何不同的策略?
答: Vue: 对模板中的响应式数据和计算属性做自动依赖跟踪,只更新变化的部分,具有自动优化特性。然而当数据量非常大或组件嵌套深时,仍可能需要手动优化(如使用v-once
、v-for
加key
、keep-alive
等)。
React: 默认更新粒度较粗(整个组件重新运行函数/render
),需要开发者手动优化。常用策略有:shouldComponentUpdate
、PureComponent
、memo
等控制不必要渲染;useMemo
、useCallback
缓存数据和函数;列表渲染时合理使用key
。React 更强调“只渲染需要变更的组件”,但依赖开发者正确使用这些优化手段。
总体而言,Vue 倾向于自动化优化而 React 倾向于提供工具给开发者主动优化。选择框架时应根据团队熟悉度和项目需求综合考虑两者在性能优化方面的特点。 -
问:React 的单向数据流与 Vue 的双向绑定有何优缺点?
答: React 的单向数据流(props down, callbacks up)使得数据流动清晰可追踪,容易调试和理解数据来源,有助于构建可预测的应用。缺点是对于表单等场景较多的手动同步代码。Vue 的双向绑定(尤其是v-model
)减少了开发者手动管理数据同步的工作量,简化表单和输入型组件的实现。但这也可能导致逻辑不够直观,某些情况下难以追踪数据流向。简而言之,单向数据流更加显式和可控,双向绑定开发方便但可维护性略差,团队应根据项目规模和复杂度选用合适的模式。
参考资料: 本题库参考了 React 官方文档、社区翻译以及技术博客等资料,例如:React 架构概览与 Fiber 引入;React 18 并发与过渡机制;Hooks 用法与原理;Vue 对比分析等。以上信息供面试复习参考。