组件与 Fiber 架构

共 52 题
📑 题目列表 52 题
#
★★★

1. React 19 的 Actions(form actions、useActionState)

React 19 的 Actions 是什么?form actions 与 useActionState 在表单提交场景中如何协作,工程上有哪些边界?

  • Actions 基于 Transition 的异步更新语义
  • <form action={fn}> 与原生表单的渐进增强集成
  • useActionState 的 pending 状态与返回更新机制

Actions 是 React 19 引入的基于 Transition 的异步函数执行机制:传给 action 的函数(如 form action、startTransition 中的函数)会被视为低优先级的 Transition 更新,React 会跟踪其 pending 状态,并自动保留上一次提交的输入值、在提交后重置表单。<form action={fn}> 把提交处理直接挂到原生表单上,action 返回 promise 后 React 自动管理 pending 与表单重置;useActionState(fn, initialState) 则把 action 与状态绑定,返回 [state, formAction, isPending],action 的返回值会成为新的 state,用于展示错误信息或结果数据,并且不依赖 JS 也能通过 server action 完成提交,实现渐进增强。

工程边界:Actions 内的 setState 被视为 Transition,因此多次快速提交会互相打断、以最后一次为准,不适合必须在提交时立即看到结果的场景(可配合 useOptimistic 展示乐观结果);action 若被其它代码直接调用而非通过 form/startTransition,则不会获得 pending 跟踪;useActionState 的状态更新由 action 返回值驱动,错误处理需在 action 内部 try/catch 后返回错误对象而非抛出。此外 form action 的 pending 状态在子组件中不可见,需要 useFormStatus 在表单内部读取。

本题考察对 React 19 表单与异步更新核心机制的掌握:答题主线是"Action = Transition + 表单集成 + 状态跟踪",再展开 form action 的渐进增强与 useActionState 的返回值驱动状态两个落点,最后用工程边界(pending 可见性、错误处理、优先级)体现实战认知。

const [state, formAction, isPending] = useActionState(async (prev, formData) => {
  const err = await submit(formData);
  return err ?? { ok: true };
}, null);
<form action={formAction}>...</form>
#
★★★

2. React Fiber 优先级调度与 Scheduler 包(shouldYield 时间切片)

React Fiber 的优先级调度如何与 Scheduler 包协作?shouldYield 与时间切片机制是如何实现的,对长任务渲染有什么价值?

  • Scheduler 的 5ms 时间片与 shouldYield 判断
  • 消息循环(MessageChannel)驱动的任务循环
  • 优先级(lane)与时间切片在并发渲染中的协作

React 的调度核心是独立的 Scheduler 包,它维护一个任务队列,用 MessageChannel 的宏任务机制驱动工作循环,避免微任务饿死主线程。每个任务在执行前通过 shouldYield() 检查当前时间片(默认约 5ms)是否耗尽:若已超时则暂停当前 Fiber 树的渲染,把剩余工作放回调度队列,让出主线程处理输入事件、动画帧等更高优先级任务,从而实现"时间切片"(time slicing)。Lane 优先级模型决定任务的插入顺序与过期时间:高优先级(如同步、离散输入)任务可插队抢占低优先级(如 Transition)任务,但同一优先级的渲染被切片后能跨宏任务断续完成。

工程价值:长列表、大树的渲染不再一次性阻塞主线程,配合并发特性(startTransition/useDeferredValue)可保证输入始终响应。边界:时间切片只在可中断的并发渲染中生效,同步渲染(如 legacy render、flushSync)仍会一次性执行到底;每个切片内依然同步执行,单个 Fiber 节点内不可中断。

答题应从"Scheduler 用 MessageChannel + 5ms 时间片 + shouldYield 让出主线程"的机制主线切入,再讲 Lane 如何决定任务插队与过期,最后落到时间切片的工程价值与同步渲染的边界,体现对并发渲染底层机制的理解。

#
★★★

3. Fiber 的 beginWork/completeWork 与 effect list 在 commit 阶段的处理

React Fiber 的 beginWork/completeWork 阶段分别做什么?effect list 在 commit 阶段是如何收集与处理的?

  • beginWork 的 diff 与子节点创建、completeWork 的属性收尾
  • 副作用(flags)的收集与 effect list 的形成
  • commit 阶段按 flags 分类执行 DOM 变更与生命周期

渲染阶段从 root 开始对 Fiber 树执行深度优先遍历:beginWork 处理当前 Fiber,依据 props/state 变化决定复用(bailout)还是新建子节点,并对元素做 diff 生成子 Fiber;completeWork 在子节点完成后向上返回,此时创建/更新对应的真实 DOM 节点、收集属性,并把本节点与子树中的副作用通过 effect list(完成时以链表形式把带 flags 的 Fiber 串联)上传给父节点。所有副作用标志(Placement、Update、Deletion、Passive、Layout 等)都记录在 Fiber 的 flags/subtreeFlags 上。

commit 阶段分三个子阶段:before mutation(读取布局、调用 getSnapshotBeforeUpdate)、mutation(执行 DOM 增删改、卸载组件、删除 ref)、layout(useLayoutEffect、componentDidMount/Update、设置 ref)。React 19 中 effect list 被 subtreeFlags 位掩码遍历取代,commit 时沿树按 flags 精确找到副作用节点执行,避免维护链表的额外成本;最后再统一异步执行 useEffect 等 Passive 副作用,并处理 Layout 与 Passive 清理函数的配对顺序。

本题考察渲染管线的整体认知:beginWork 向下构建、completeWork 向上收尾并收集副作用、commit 按 flags 分类执行,这是 React 双阶段工作流的标准答案;补充 subtreeFlags 对 effect list 的替代可体现对 React 19 内部演进的了解。

#
★★★

4. Fiber 架构的双缓冲(current/workInProgress)

Fiber 架构的双缓冲(current/workInProgress)是什么?为什么 React 要采用双 Fiber 树?

  • current 树与 workInProgress 树的角色分工
  • 可中断渲染与提交切换(alternate 指针)
  • 双缓冲对内存复用与一致性读视图的价值

React 在内存中同时维护两棵 Fiber 树:current 树对应已提交到屏幕的 UI,workInProgress 树是本次更新在后台构建的新树,两树节点通过 alternate 指针相互关联。渲染开始时从 current 树的节点克隆出 workInProgress 节点,在可中断的渲染过程中所有工作都在 workInProgress 上完成,即使中途被高优先级打断丢弃,current 树依然完整,页面不会出现半渲染状态。当整棵树构建完成并提交时,root 的 current 指针一次性切换到 workInProgress 树,旧树成为下次构建的缓冲,完成"双缓冲交换"。

工程价值:一是保证"提交前永远有一棵完整一致的树",支持并发可中断渲染;二是节点对象复用(交替使用两棵树,减少频繁 GC);三是提交是原子的指针切换,DOM 变更集中在 commit 阶段一次性完成,避免用户看到中间态。边界:初次渲染没有 current 树,workInProgress 直接新建;两棵树各自的 hook 链表通过 alternate 保持状态对应。

双缓冲是并发渲染的地基:答题抓住"current 展示、workInProgress 构建、提交时指针切换、节点 alternate 复用"四个要点,再解释其对可中断渲染与原子提交的支撑,即可覆盖本质与工程价值。

#
★★★

5. Concurrent Rendering(createRoot)

React 19 的 createRoot 与并发渲染是什么关系?启用并发渲染后,渲染行为发生了哪些变化?

  • createRoot 与 legacy render 的区别
  • 可中断渲染与自动批处理(Automatic Batching)
  • 并发特性(Transition、Suspense)的启用条件

createRoot 是 React 18/19 并发渲染的入口:createRoot(container).render(<App/>) 创建根节点并启用并发特性,取代 legacy 的 ReactDOM.render。并发渲染意味着渲染可被更高优先级任务中断、恢复与放弃,更新按 lane 优先级调度;React 19 中并发渲染已是默认模式,同时自动批处理扩展到了 promise、setTimeout、原生事件处理器等所有场景,多次 setState 会合并为一次渲染。

并发渲染带来的行为差异:渲染不再同步阻塞主线程(可中断);Suspense、startTransition、useDeferredValue、useTransition 等并发特性全部可用;渲染阶段的生命周期(如 render 函数、函数组件体)可能被多次调用,因此不能在其中做副作用;useEffect 在 StrictMode 下双调用以暴露非纯渲染。工程边界:flushSync 可强制同步刷新以处理必须立即同步的 DOM 操作(如测 DOM 尺寸),但会打断并发渲染,需谨慎使用。

答题主线是"createRoot 是并发渲染的开关,渲染可中断 + 自动批处理是两大行为变化",再补充渲染纯函数要求与 flushSync 边界,体现从 API 到行为差异的完整理解。

#
★★★

6. useId 与 Concurrent Rendering/Transition 的协作

useId 的作用是什么?为什么并发渲染下不能依赖递增计数器生成 ID,useId 与 Transition 协作时有什么工程要点?

  • useId 的生成原理(组件树路径编码)
  • 并发渲染可中断导致的 ID 不一致问题
  • SSR 水合一致性与 Transition 下的 ID 稳定性

useId 返回一个基于组件树层级与父 ID 拼接而成的稳定唯一字符串(形如 :r1:),用于生成无障碍属性、label 与输入框的关联 id 等。它的设计目标就是对抗并发渲染:渲染可被中断、重放,若用模块级递增计数器生成 ID,中断后恢复会导致同一组件在不同次渲染中得到不同 ID,而 useId 的 ID 只取决于组件在树中的位置,与渲染次数无关,从而保证稳定。在 Concurrent Mode 下同一个组件可能渲染多次,但 useId 结果始终一致;配合 Transition 中断高优先级更新时,已渲染的 UI 中的 ID 不会错位。

工程要点:useId 只能生成唯一标识,不保证语义,也不适合作为 key 使用(key 需要基于数据而非位置);不能在循环中为每个子项调用 useId 生成 key;SSR 时服务端与客户端必须按同一树结构生成,才能保证水合时 ID 一致。React 19 中为每个模块分配前缀(如 :R_R)以区分不同模块复制的组件树,进一步降低 ID 冲突。

本题核心是"useId 的确定性来自树位置而非渲染次数":先讲生成原理,再对比计数器方案在可中断渲染下的失效,最后落到 SSR 水合一致性与不做 key 的边界,形成完整答题链。

#
★★★

7. React Lane 模型(31 个 lane 位掩码)在并发更新的工程价值

React 的 Lane 模型是什么?为什么用 31 个 lane 位掩码表达优先级,它在并发更新中的工程价值有哪些?

  • Lane 位掩码的数据结构与优先级划分
  • 过期时间(expiration)与插队/打断机制
  • 批处理合并与 Transition 低优先级表达

Lane 模型用 31 位二进制数表示更新优先级,每位(lane)代表一个更新,通过位运算(如 lane & lanes)实现优先级的快速比较、合并与批处理,比数字比较更高效地表达"更新集合"。按位序划分为 SyncLane(同步)、InputContinuousLane(连续输入)、DefaultLane(默认)、TransitionLane(过渡)等优先级带,Transition 内部还有多级子 lane 用于表达多次过渡更新的渐进降级。每个更新进入 fiber 时被打上 lane,调度器取"未过期且优先级最高"的任务优先执行,高优先级 lane 可打断低优先级 lane 的渲染。

工程价值:位掩码让并发更新可批量合并(同一优先级的多次更新合并为一次渲染)、可插队(输入事件打断 Transition)、可过期重试(低优先级任务超时后强制同步执行,避免饿死);Transition 多级 lane 实现了"先完成一部分、剩余降级处理"的渐进过渡体验。这是 React 能精确表达"输入必须即时响应、后台更新可被无限打断"的基础设施。

答题抓住"位掩码 + 优先级带 + 可打断/可过期"三个层次:先讲数据结构,再讲调度语义(插队、合并、过期),最后落到 Transition 渐进降级的工程价值,体现对并发内核的理解深度。

#
★★★

8. React 19 的 组件与原生 View Transitions API 集成

React 19 的 组件是什么?它与浏览器原生 View Transitions API 如何集成,工程上有哪些注意点?

  • 的声明式用法与 startViewTransition 的关系
  • 快照动画与 React 更新时序的配合
  • prefers-reduced-motion 与渐进增强

是 React 19 提供的声明式组件,把浏览器原生 View Transitions API(document.startViewTransition)与 React 更新绑定:在组件内触发更新时,React 自动把这次更新包进 startViewTransition 的回调中,浏览器先截取旧视图快照、执行 DOM 更新、再截取新视图快照,并对两帧做默认的交叉淡入过渡,开发者可通过 ::view-transition-* 伪元素定制动画。它与 startTransition 命名相近但语义不同:前者管视觉过渡,后者管更新优先级;二者可组合使用。

工程注意点:View Transitions 需要浏览器支持(Chromium 系),需特性检测后降级为普通更新;过渡期间浏览器会放大根快照,可能影响滚动位置与可访问性,配合 prefers-reduced-motion 媒体查询关闭动画;由于是两帧快照,动画时长内 React 更新导致的中间态不可见,适合路由切换、主题切换等整块内容替换场景,不适合高频细粒度更新。

答题主线是" = React 声明式封装原生 startViewTransition,更新被包进过渡回调",再补充快照动画原理、特性检测降级与减少动效偏好三个工程点,覆盖 API 到实践的完整认知。

<ViewTransition>
  <div key={theme}>{theme === 'dark' ? <DarkApp/> : <LightApp/>}</div>
</ViewTransition>
#
★★★

9. Concurrent Mode 与可中断渲染的边界

Concurrent Mode 的可中断渲染有哪些边界?哪些渲染不可中断,哪些场景需要避免使用并发特性?

  • 可中断渲染的适用条件(Transition 与低优先级)
  • 同步渲染与 flushSync 的不可中断边界
  • 副作用与外部库对可中断渲染的约束

可中断渲染只发生在并发更新中:以 startTransition/useDeferredValue/useTransition 标记的更新、以及 createRoot 下的默认更新都可被更高优先级任务中断、恢复或丢弃,而同步更新(flushSync 内的更新、事件中的紧急更新在部分场景)会一次性执行到底。可中断意味着同一组件的渲染函数可能执行多次,因此渲染期间不能有副作用:DOM 写入、订阅、请求必须放到 useEffect/useLayoutEffect 中,否则会被重复执行或产生不一致。

边界约束:外部非 React 库(如手动操作 DOM、直接操作 DOM 的图表库、同步读取布局的代码)无法感知 React 的中断与恢复,若依赖渲染时序会出错,需通过 ref 与 effect 隔离;对需要立即反映用户输入的场景(输入框即时过滤、拖拽)应使用紧急更新或直接 setState,而非把关键路径包进 Transition;服务端渲染不受并发渲染中断影响,但水合时需要客户端与服务端输出一致。

本题考察"并发是有边界的"这一核心认知:先给出可中断的适用域(并发更新)与不可中断域(同步/flushSync),再强调渲染纯度要求与外部库隔离两个实践边界,体现辩证的工程理解。

#
★★★

10. React 渲染阶段(Render、Commit)与副作用时机

React 的 Render 与 Commit 阶段分别做什么?不同副作用(生命周期、useEffect、useLayoutEffect)的调用时机有何差异?

  • Render 阶段的纯函数要求与可中断性
  • Commit 阶段的 mutation/layout 子阶段
  • useEffect 与 useLayoutEffect 的时序差异

Render 阶段从 root 出发构建/更新 Fiber 树,计算新的 props/state、执行组件函数(函数组件体)或 render 方法、进行 diff,此阶段必须纯净:不能修改 DOM、不能读写外部可变状态,因为它可被中断、重放(并发模式)或在 StrictMode 下双调用。Commit 阶段把计算结果一次性应用到 DOM:先执行 before mutation(getSnapshotBeforeUpdate),再执行 mutation 阶段同步插入/更新/删除 DOM 节点,然后执行 layout 阶段(useLayoutEffect、componentDidMount/Update/Unmount 等),此时 DOM 已更新但浏览器尚未绘制,可同步读取布局。

useEffect 在 commit 后异步执行(浏览器绘制之后),不阻塞绘制,适合订阅、网络请求、日志等非关键副作用;useLayoutEffect 在 commit 同步执行(绘制前),适合测量 DOM、同步样式调整,但会阻塞绘制,应谨慎使用;两者的清理函数分别在其下次执行前与卸载时调用。规律:越接近"必须与 DOM 同步"的副作用越靠前执行,优先级从"渲染期间不可做"到"layout 同步"到"绘制后异步"逐级放宽。

答题按"Render 纯函数 + Commit 应用 DOM"两阶段框架展开,再把副作用按时机分层:layout 同步(绘制前)、effect 异步(绘制后),并用测量 DOM 与订阅两个典型场景说明取舍,即可覆盖考点。

#
★★★

11. React 19 Activity 在隐藏子树保留状态/停止渲染的工程价值

React 19 的 Activity 组件是什么?它在隐藏子树、保留状态与停止渲染方面的工程价值如何体现?

  • Activity 与 Offscreen 的前身关系及 mode 属性
  • 隐藏时保留 state 与停止渲染的机制
  • 保活页面与 Tab 工作台的工程应用

Activity(前身是实验的 )是 React 19 的官方组件,通过 mode="visible" | "hidden" 控制子树:切到 hidden 时子树的内容从 DOM 中"隐藏"但保留其 state(不卸载、不丢失滚动位置与表单输入),同时 React 会停止对其渲染更新(除非收到高优先级更新),减少 CPU 与内存开销;切回 visible 时无需重新挂载,恢复即时。它内部用 display 隐藏而非卸载,并在协调层记录"已隐藏"标记,跳过对隐藏树的常规渲染工作。

工程价值:多 Tab 工作台、路由保活、Modal 抽屉等"隐藏但需保留状态"的场景无需再依赖 KeepAlive 类库或手动缓存组件树;隐藏树中的 useEffect 默认在隐藏时暂停执行(React 19.2 的 Activity 让 Effect 与后台更新也可配置),避免后台页面持续消耗资源。边界:hidden 子树的 DOM 仍存在,页面体积与内存不会完全释放,需与真正的卸载做成本权衡;隐藏树中不应依赖持续可见的定时器/动画。

答题主线是"Activity 隐藏而不卸载、保留 state 且停止渲染",再落到保活场景的工程价值与"DOM 仍占用、效果暂停"的边界,体现对状态保留与资源治理双面性的理解。

#
★★★

12. React 19.1+ useDeferredValue 的 initialValue/compare 在表单优化的工程价值

React 19.1+ 的 useDeferredValue 新增了 initialValue 与 compare 参数,它们各有什么作用?在表单优化中如何应用?

  • initialValue 解决 SSR 水合时的值闪烁
  • compare 自定义延迟值更新触发条件
  • 表单过滤、搜索场景的降级渲染

React 19.1 为 useDeferredValue 增加了两个参数:initialValue 在首次渲染(含 SSR 水合)时作为初始延迟值返回,避免 useDeferredValue 在客户端水合时先用真实值渲染、再切换回延迟值的"闪烁"(尤其服务端渲染的搜索框在首帧显示完整列表、水合后又切回已过滤列表);compare 用于自定义旧值与新值的比较逻辑,默认是 Object.is,当 compare 返回相等时 React 跳过延迟值的更新,例如按对象 id 而非引用比较,避免因引用变化导致不必要的降级渲染。

表单优化中的应用:搜索输入框把值传给 useDeferredValue,输入保持即时响应,昂贵的结果列表用延迟值渲染并在背后更新,实现"输入不被卡顿";配合 compare 可在值"语义未变"(如仅空格变化)时跳过重新渲染;initialValue 则保证 SSR 首屏与客户端水合一致。边界:延迟值仍会最终更新,不适合需要立即反映的受控结果(如必须同步的校验提示),且 React 不保证延迟渲染一定会在超时前完成,可能被高优先级输入持续打断。

本题考察对新 API 参数的精确理解:initialValue 解决水合闪烁,compare 控制更新触发粒度,二者分别对应"首屏一致性"与"渲染去重"两个工程痛点,结合搜索表单场景说明即可。

#
★★★

13. React 协调过程中 useMemo/memo/useCallback 的依赖数组 Object.is 比较差异

React 协调过程中 useMemo、memo、useCallback 的依赖数组比较有何差异?Object.is 比较对缓存失效有什么影响?

  • Object.is 与 === 的差异及对引用类型的判断
  • 三个 API 的缓存失效条件
  • 依赖数组变更对重渲染与重计算的传导

三个 API 的依赖比较都使用 Object.is(与 === 基本等价,区别是 Object.is(NaN, NaN) 为 true、Object.is(-0, 0) 为 false)。useMemo 在依赖变化时重新执行工厂函数并返回新值,否则复用旧值;useCallback 等价于 useMemo(() => fn, deps),返回稳定函数引用;React.memo 对 props 做浅比较(内部同样基于 Object.is 逐 key 比较)。由于是浅引用比较,依赖数组中的对象、数组、函数每次渲染都是新引用,会导致缓存失效——这正是"依赖数组里放了非稳定引用,useMemo/useCallback 形同虚设"的经典问题。

协调过程中的传导:父组件每次渲染,内联对象/函数产生新引用 → 子组件 useMemo 依赖变化重新计算、memo 的 props 浅比较不相等而重渲染;React 19 中即使 memo 包裹,若 props 中任何值引用变化仍会重渲染。工程实践:依赖应只放"语义稳定"的值(原始类型、useRef 持有的稳定引用、useMemo 产出的稳定对象),或用 React Compiler 自动记忆化替代手工依赖管理;React 19 中 memo 的显式用法正在被编译器自动记忆化取代。

答题核心是"依赖比较 = Object.is 浅比较 + 引用稳定性":先讲比较语义,再讲三个 API 各自失效条件,最后用"非稳定引用导致缓存失效"的传导链说明工程取舍,层次分明。

#
★★★

14. useTransition/useDeferredValue 与 startTransition 在并发模式的应用

useTransition、useDeferredValue 与 startTransition 在并发模式中各自的应用场景是什么?如何取舍?

  • 三个 API 的语义区别(是否受控、返回值)
  • 输入响应性与后台更新的优先级取舍
  • 各自适用的典型场景

三个 API 都用于把更新标记为低优先级的 Transition,但用法不同:startTransition(fn) 是命令式入口,在事件回调中包裹"非紧急"的状态更新,不返回 pending 状态;useTransition 返回 [isPending, startTransition],可读取过渡是否进行中,用于展示加载态或禁用按钮;useDeferredValue(value) 是声明式用法,不直接包裹更新,而是把某个值延迟更新(相当于用过渡优先级渲染依赖该值的部分),适合"输入即时、结果延后"的场景,无法手动控制何时开始。

取舍:用户输入本身必须紧急更新(保证输入框即时响应),依赖输入派生的大开销渲染(过滤大列表、图表、复杂 UI)放入 Transition 或使用 deferred 值;startTransition/useTransition 适合"在事件中明确划分紧急与后台更新",useDeferredValue 适合"值驱动、不便于包函数"的渲染场景(如第三方组件只接收值)。边界:Transition 中的 setState 仍会最终完成(可被打断但会重试),不能依赖其完成时序;对必须在提交后立即读 DOM 的代码需注意 Transition 更新的异步性。

答题先区分三个 API 的形态差异(命令式 vs 声明式、有无 pending),再给出"输入紧急 + 派生渲染低优"的黄金组合,最后用典型场景与边界收尾,体现并发特性的工程应用能力。

#
★★★

15. React 19 的 Server Components 与 Client Components 的 RSC Payload 在流式 SSR 中的边界

React 19 的 Server Components 与 Client Components 在 RSC Payload 的流式 SSR 中如何协作?两者的边界如何划分?

  • RSC Payload(Flight 格式)的序列化内容
  • 流式 SSR 中 Suspense 边界的暂停与恢复
  • "use client" 边界与可序列化 props 约束

Server Components 只在服务端执行,不打包进客户端 JS,其输出不是 HTML 而是 RSC Payload(Flight 协议):包含组件树的描述、传给 Client Components 的 props(必须可序列化:原始类型、Date、Map、Promise 等)、以及被 Suspense 挂起时以"延迟占位 + 后续补发"方式流式传输的片段。客户端拿到 Payload 后把 Client Components 水合为交互 UI,Server Components 部分则直接以服务端渲染的 HTML/静态内容呈现,不参与水合。流式 SSR 中,每个 Suspense boundary 是一个传输边界:阻塞的子树先发送 fallback,数据就绪后再流式补发对应 HTML 与 Payload 片段,客户端渐进呈现。

边界划分:"use client" 标记的文件及其依赖进入客户端包,只能接收可序列化 props;Server Components 可读取数据库、文件、环境变量等 Node 能力;跨边界的函数无法直接传递(除 Server Actions),需通过 server function 引用;RSC 与 SSR 是两条独立的流(Payload 流与 HTML 流),React 19 中它们由同一渲染管线协同输出,客户端用 payload 完成水合与更新,HTML 保证无 JS 时的首屏。

本题考察 RSC 双通道流式架构:Server Components 输出 Payload、Client Components 负责交互、Suspense boundary 是流式暂停恢复的边界,回答需同时覆盖序列化约束与渐进呈现两个层面。

#
★★★

16. React Fragment(<>/React.Fragment)与 key 在列表渲染的应用

React Fragment 与短语法 <> 在列表渲染中如何使用?key 的作用与正确用法是什么?

  • Fragment 不产生额外 DOM 节点
  • 列表渲染中 key 的标识作用与短语法带 key 的限制
  • key 的唯一性与稳定性要求

Fragment 允许组件返回多个兄弟节点而不创建额外 DOM 节点,完整写法 React.Fragment 可带 key 属性,短语法 <>...</> 不能带 key 也不能带属性(React 会忽略),因此循环中使用 或把 key 放在 Fragment 上。在列表、表格(td 不能包裹 div)、flex/grid 布局等"不能有中间容器"的场景中,Fragment 是唯一不破坏结构的组合方式。

key 用于在协调(diff)中标识列表项的身份:React 依据 key 判断节点是移动、复用还是重建,从而保留组件 state 与 DOM。正确用法:key 必须唯一、稳定且基于数据(如业务 id),不要用数组索引(索引在增删时会错位导致状态复用错误)、不要用 Math.random 之类每次变化的随机值(会导致每次渲染全部重建)。key 只在兄弟节点之间比较,不需要全局唯一;跨列表的相同 key 互不影响。

本题是列表渲染的基础综合题:Fragment 解决"无中间节点"需求,key 解决"节点身份识别",答题时分别给出语法细节(短语法不能带 key)与 key 的三大属性(唯一、稳定、基于数据),并解释索引 key 的危害。

{items.map(item => (
  <Fragment key={item.id}>
    <dt>{item.name}</dt>
    <dd>{item.desc}</dd>
  </Fragment>
))}
#
★★★

17. JSX 的 children 渲染(Children API,React.Children.map/forEach/toArray)在动态子元素的边界

React.Children 的 map/forEach/toArray 等 API 在动态子元素处理中的边界是什么?与原生数组方法有何区别?

  • Children API 对不透明 children 结构的归一化
  • 与数组方法的差异(null/单元素/嵌套)
  • 现代 React 中 Children API 的弃用趋势

React 元素的 children 是不透明的:可能是单个元素、数组、嵌套数组、字符串、null 或 Fragment。React.Children.map/forEach 会递归展开嵌套数组并把单元素归一化为数组语义,同时自动过滤 null/undefined 与布尔值,且不改变原 children(返回新数组),让开发者无需手动处理各种形态;React.Children.toArray 把 children 拍平为带 key 的数组,用于需要排序、截断等操作后仍保持 key 稳定。而原生 Array.prototype.map 只能处理已经是数组的情况,无法处理单元素或嵌套结构。

边界与趋势:Children API 的归一化会隐藏 children 的真实结构,可能影响性能(递归遍历);children 若含非数组可迭代对象、React.Fragment 会被当作数组展开处理;React 19 文档明确建议"尽量不用 Children API"——若需遍历请先 toArray,因为 children 实际是数组/迭代器形态,且 Children API 与现代编译器/并发特性协作不佳;识别"是否只有单个子元素"可用 React.isValidElement 而非 Children.count 的过度展开。大多数场景下把 children 当作只读数据、用 cloneElement 与 key 处理即可。

答题主线是"Children API 把不透明 children 归一化为数组语义(递归展开、过滤空值)",再对比原生数组方法说明其必要性,最后落到 React 19 官方"少用 Children API、优先 toArray"的现代趋势,体现对演进方向的把握。

#
★★★

18. 受控组件与非受控组件的边界

受控组件与非受控组件的本质区别是什么?各自的适用边界与 React 19 的新变化有哪些?

  • 数据源归属:React state 与 DOM 自身状态的边界
  • 受控模式的完整闭环与受控警告
  • React 19 中 ref 可作 prop 对非受控实现的影响

受控组件把表单值交给 React 管理:value 由 state 提供,onChange 更新 state,渲染值始终等于 state,数据流单向可预测;非受控组件把值保存在 DOM 自身(input 内部维护 value),React 只通过 defaultValue 设置初始值,需要时用 ref 读取当前值。受控模式的优点是数据与 UI 完全同步、便于校验/联动/重置,缺点是每次按键都触发渲染;非受控模式减少渲染与受控连动,适合简单表单或与第三方库(RHF 的 register + ref)协作。

边界:受控组件若只设 value 不设 onChange,输入会立即被 React 重置回 state(控制反转失效);React 会在受控切换为非受控时警告。工程上,大型表单常用"非受控 + ref 读取"(react-hook-form)以性能优先,需要联动校验的字段用 Controller 转受控;React 19 中 ref 可以作为 prop 直接传给函数组件,非受控组件无需 forwardRef 也能暴露 ref 给父组件读取 DOM 值。判断标准:值由谁持有——state 持有即受控,DOM 持有即非受控,两者边界即"数据流的拥有权"。

答题以"值的拥有者"为总纲:React state 持有即受控、DOM 持有即非受控,再展开各自优缺点、受控警告与 RHF 非受控模式的工程取舍,最后提及 React 19 ref-as-prop 的简化,构成完整答案。

#
★★★

19. 函数组件与 Class 组件的差异与现代取舍

函数组件与 Class 组件在实现机制上有哪些本质差异?现代 React 工程中如何取舍?

  • 生命周期与 Hooks 的映射关系
  • this 绑定与闭包捕获的差异
  • 现代 React 对函数组件的官方导向

函数组件以函数调用 + Hooks 表达状态与副作用,Class 组件以实例 + 生命周期方法表达。本质差异有三:一是状态与副作用模型不同——Class 用 this.state/this.setState 与 componentDidMount/Update/Unmount 等生命周期,函数组件用 useState/useEffect,且 useEffect 把"挂载/更新/卸载"统一为"依赖变化执行 + 清理"的声明式描述;二是 this 语义——Class 的方法依赖实例绑定(回调中 this 丢失需 bind 或箭头函数),函数组件每次渲染都捕获当次渲染的 props/state 闭包,天然避免了 this 陷阱但也带来了闭包过期(stale closure)问题;三是复用能力——Class 的复用依赖 HOC 与 render props(易产生嵌套地狱),函数组件用自定义 Hook 组合,逻辑抽取与共享更自然。

现代取舍:React 官方与生态全面转向函数组件 + Hooks,新代码默认函数组件;Class 组件仍被支持(React 19 未移除),但不再有新特性(如并发特性中的部分优化、React Compiler 自动记忆化主要面向函数组件);存量 Class 代码可逐步迁移,迁移时注意生命周期到 useEffect 的语义映射(尤其 getSnapshotBeforeUpdate 对应 useLayoutEffect、componentDidCatch 对应 error boundary 类组件)。性能上两者无本质差异,选择取决于团队代码库与新特性诉求。

答题从"状态模型、this/闭包、复用机制"三个本质差异切入,再落到官方导向与新特性(Compiler、并发)对函数组件的倾斜,最后给出迁移建议,体现机制理解与工程判断。

#
★★★

20. JSX 编译为 React.createElement 的过程

JSX 是如何编译为 React.createElement 的?新 JSX Transform 与旧版有何不同?

  • JSX 到 createElement 调用的映射规则
  • 新 JSX Transform 的 jsx/jsxs 与自动引入
  • 编译产物的结构与 key/children 的处理

JSX 是语法糖:<div className="a">text</div> 经 Babel/TypeScript 编译为 React.createElement("div", { className: "a" }, "text")。createElement 接收类型(字符串标签或组件引用)、props 对象与 children,返回描述 UI 的 React 元素对象 { type, key, ref, props };标签名小写编译为字符串(原生 DOM),首字母大写编译为组件变量;属性名做转换(className、htmlFor、camelCase 事件);多个子节点作为剩余参数传入,数组子节点自动嵌套;key 与 ref 从 props 中提出为元素顶层字段,不进入 props(ref 在 React 19 中例外,可作为普通 prop 传递)。

新 JSX Transform(React 17+):不再要求源码中 import React 即可使用 JSX,编译产物从 createElement 改为 jsx/jsxs(自动导入自 react/jsx-runtime,jsxs 针对多子节点有编译期优化),减少了每个模块对 React 全局的依赖,也降低了 bundle 体积(dev/prod 使用不同入口,prod 下 jsxDEV 被精简)。React 19 继续使用该 transform,同时 JSX 中不再需要显式 import React,只有用到 Hooks 才需要。

本题考察编译链路的基础知识:先讲 JSX → createElement 的映射(类型、属性、children、key/ref 分流),再讲新旧 transform 的差异(react/jsx-runtime、自动引入、jsxs 优化),体现从语法到编译产物的理解。

#
★★★

21. 边界与 React.lazy 的代码分割协作

边界如何与 React.lazy 协作实现代码分割?fallback 的展示与恢复机制是什么?

  • React.lazy 的动态 import 与组件加载
  • Suspense 边界的 fallback 显示与恢复
  • 代码分割的最佳实践(粒度与边界位置)

React.lazy(() => import('./Heavy')) 把组件变成"挂起"型组件:首次渲染时组件不在 bundle 中,渲染过程会抛出一个 Promise(表示"还在加载"),最近的 边界捕获该 Promise 并渲染 fallback(如 loading 占位),加载完成后 React 重新渲染并展示真实组件,无需手动管理 loading 状态。这实现了"路由级/组件级代码分割":按需下载 JS,首屏只加载必要代码。

工程要点:Suspense 边界应放在"加载后不变化的 UI"外围(通常每个路由一个边界),避免 fallback 切换引起布局抖动;React.lazy 目前只能配合 Suspense 使用,SSR 中 React 19 的 Suspense 支持流式渲染 lazy 组件;多个 lazy 组件可共用一个边界;代码分割粒度以"路由或大块业务"为宜,过细会增加请求数并稀释缓存收益;配合 webpack/Vite 的预取(prefetch)与 React 19 的 preload 可在空闲时预下载,进一步提升体验。Suspense 同时承担数据获取的等待(RSC、Suspense for Data Fetching),lazy 只是其一个应用。

答题核心是"lazy 抛 Promise → Suspense 捕获 → fallback → 加载完成恢复"的协作闭环,再展开代码分割的粒度与边界放置原则,最后把 Suspense 泛化到数据获取,体现纵深理解。

const Chart = React.lazy(() => import('./Chart'));
<Suspense fallback={<Spinner/>}>
  <Chart/>
</Suspense>
#
★★★

22. cloneElement 在高级组件(HOC)与 Render Props 的取舍

cloneElement 的作用是什么?在 HOC 与 Render Props 模式中如何使用,React 19 下有什么取舍?

  • cloneElement 复制元素并合并 props/key/ref 的语义
  • HOC 与 Render Props 的 props 注入方式
  • 现代替代方案(组合、context、children 作为函数)

cloneElement(element, props, ...children) 复制一个 React 元素并浅合并新 props(新 props 覆盖旧值,children 替换),常用于向子元素注入 props、改写 key/ref、或给 children 附加事件与数据。在 HOC 中,包装组件通过 cloneElement(children, extraProps) 把外部数据注入子元素;在 Render Props 中,children 作为函数接收渲染数据,一般不依赖 cloneElement,而是把数据作为函数参数传入,由子组件自由渲染。

React 19 的取舍:cloneElement 浅合并 props 会丢失子元素内部已有的某些配置细节(如 ref 的传递方式变化),且它操作的是"元素"而非真实组件,无法在克隆后保证子组件重新执行 render 或拿到最新 context(cloneElement 只在渲染期有效,props 是一次性快照);现代实践更推荐:用 context 共享隐式数据、用组合(children 插槽)代替注入、用自定义 Hook 传递逻辑;若必须注入 props,优先考虑"由子组件自己读取数据"或 render prop 模式。cloneElement 在跨库适配、表单库包装字段等场景仍有价值,但应避免作为通用数据传递手段。

答题先讲 cloneElement 的复制与合并语义,再对比 HOC 与 Render Props 两种注入方式,最后给出 React 19 下"优先组合/context/Hook,cloneElement 仅用于特殊包装"的现代取舍,展现演进认知。

#
★★★

23. React 19 use 在 Promise/Context 读取的工程边界(Suspense 协作)

React 19 的 use() 如何读取 Promise 与 Context?它与 Suspense 的协作及工程边界是什么?

  • use(Promise) 的挂起语义与条件调用
  • use(Context) 与 useContext 的等价与差异
  • 不能在 try/catch 中捕获挂起、渲染期资源读取约束

use 是 React 19 新增的渲染期资源读取 API:use(promise) 在 Promise 未就绪时使组件"挂起",由最近的 Suspense 边界渲染 fallback,Promise resolve 后重新渲染并返回结果;use(context) 等价于 useContext 读取 context 值。与 Hooks 的关键区别是:use 可以在条件、循环中调用(不要求顶层调用、不要求固定顺序),因为它不依赖 Hook 调用顺序,只依赖被读取的值,这让条件性资源读取(如根据 props 决定读哪个 context/promise)成为可能,但 hook 规则仍然约束其它 Hooks。

工程边界:use(promise) 挂起时抛出的 Promise 不能被 try/catch 捕获(会被 Suspense 拦截),错误处理应使用 Error Boundary 而非 catch;use 不能用于读取已 resolve 的任意 Promise 的"同步值",React 不缓存 Promise,重复渲染会重新调用 use 读取同一 Promise 对象;在事件处理、effect 等非渲染代码中不能调用 use。use(Context) 的组件在 context 变化时会重新渲染,与 useContext 行为一致;若与并发特性配合,use(Promise) 是 RSC 客户端消费 Flight 数据流的基础(use 可在客户端读取服务器下发的 promise)。React 19 中 use 还支持在条件分支中调用而不违反 hook 规则,这是它与传统 Hook 最大的边界差异。

答题核心是"use 是渲染期读取器:可条件调用、挂起交给 Suspense、错误交给 Error Boundary"三点,再对比 useContext 与 Hooks 规则的差异,最后强调不能在事件/effect 中调用与 Promise 不缓存的边界。

#
★★

24. React 19 Performance Tracks 在 React DevTools 显示长任务的工程价值

React 19 的 Performance Tracks 是什么?它如何在 DevTools 中帮助定位长任务与性能问题?

  • Performance Tracks 的渲染时间线数据
  • 长任务与更新来源的关联分析
  • 与浏览器 Performance 面板的配合

React 19 在 React DevTools 的 Profiler 中新增 Performance Tracks,以时间线形式记录每次提交(commit)的渲染耗时、调度事件(dispatch)、Suspense 暂停、Transition 与渲染任务切片,展示哪些更新导致长任务(long task)、每个组件在其中的渲染时长,并把更新时间线与 React 调度事件(如 input、transition)关联,帮助回答"刚才的卡顿是谁引起的"。与浏览器 Performance 面板互补:后者给出系统级主线程活动,前者给出 React 内部更新的归属与优先级(lane)。

工程价值:定位长列表/大组件树的性能瓶颈时,先用 Performance Tracks 找到耗时的 commit 与组件,再结合 React.memo、useMemo、并发特性或 React Compiler 优化;还可观察 Transition 是否如预期让出主线程、Suspense fallback 是否频繁闪烁。注意:Profiler 记录本身有开销,建议在开发环境或采样场景使用;生产性能问题可用 React 19 的 profiling bundle 与 Transition Tracing API 补充数据。

本题考察性能调试工具链:Performance Tracks 把"提交耗时、组件耗时、调度事件"关联成时间线,与浏览器面板配合形成"系统级 + React 级"双层定位,答题围绕定位长任务归属与优化闭环即可。

#
★★

25. React 元素(Element)与组件实例(Instance)

React 元素(Element)与组件实例(Instance)的区别是什么?二者在渲染流程中的角色如何?

  • 元素的不可变描述性与实例的运行时状态
  • 从元素到 Fiber 实例的构建过程
  • key/ref/type 在二者间的对应关系

React 元素是 createElement/JSX 返回的普通对象 { type, key, ref, props },是"要渲染什么"的不可变描述,不包含运行时状态,可被任意创建与丢弃;组件实例是渲染过程中为元素创建的运行时实体:函数组件对应 Fiber 节点(保存 state、hooks、props、effects),Class 组件对应类实例(this.state、this.props、refs)。渲染流程中,React 协调器遍历元素树,为每个元素创建/复用对应的 Fiber(实例),元素是"声明",Fiber/实例是"实现"。

区别要点:元素可被 cloneElement 修改、可被重复渲染产生新对象,实例则在提交后持久存在并持有状态;元素的 type 决定实例类型,key 决定实例复用匹配,ref 挂到实例(React 19 中函数组件 ref 作为 prop 接收)。元素与实例无一一对应关系:同一元素可在不同位置渲染成不同实例,同一实例可通过 key 匹配复用于不同元素。理解二者的分层(声明层 vs 实现层)是理解 reconciliation、Hooks 状态持久化的基础。

答题用"声明 vs 实现"二元框架:元素是轻量描述、可丢弃;实例/Fiber 持有状态、持久存在;再以 key/ref/type 的对应关系与"元素可复用、实例有状态"收束,覆盖概念本质。

#
★★

26. Error Boundary 的捕获边界与 19 的错误处理回调(onCaughtError/onUncaughtError/onRecoverableError)

Error Boundary 的捕获边界是什么?React 19 新增的错误处理回调 onCaughtError/onUncaughtError/onRecoverableError 各有什么用途?

  • Error Boundary 只能捕获渲染期/生命周期错误
  • React 19 三个错误回调的触发时机
  • 事件处理器错误与异步错误的处理

Error Boundary 是类组件通过 componentDidCatch/getDerivedStateFromError 实现的错误边界,只能捕获其子树在渲染、生命周期与构造函数中抛出的错误,不能捕获:自身抛出的错误、事件处理器中的错误、异步代码(setTimeout/promise 回调)中的错误、SSR 中的错误。React 19 中错误边界可重置 key 重新挂载子树(reset 或 key 变化),并新增三个根级回调:onCaughtError 在错误被 Error Boundary 捕获时调用(可用于上报)、onUncaughtError 在无边界捕获时调用(替代默认的 window.onerror 崩溃处理,可阻止默认上报)、onRecoverableError 在 React 自行恢复的错误(如水合不匹配、部分渲染错误)时调用,三者配合 createRoot 的 options 传入,形成统一错误上报管线。

边界实践:事件处理器错误应使用 try/catch 或错误上报库,异步错误用 promise.catch,这些不会触发 Error Boundary;渲染期错误兜底 UI 用 Error Boundary + key 重置;生产环境建议把三个回调接入监控平台,并关闭默认的 console 上报。React 19 还支持 Error Boundary 在 Suspense 挂起恢复失败时兜底(与 Suspense 协作)。

答题先明确 Error Boundary 的捕获域(渲染/生命周期,不含事件与异步),再逐一说明 React 19 三个回调的触发时机与上报用途,最后给错误分级处理建议,覆盖边界与新增 API。

#
★★

27. React 19 的 useOptimistic 在乐观更新(购物车、点赞)的工程实践

useOptimistic 如何实现乐观更新?在购物车、点赞等场景的工程实践与回滚策略是什么?

  • useOptimistic 的声明式乐观状态
  • 与 Action/Transition 的提交确认协作
  • 失败回滚与多请求竞态的边界

useOptimistic(state, updateFn) 返回 [optimisticState, setOptimistic]:在 Transition/Action 进行期间,setOptimistic 用 updateFn 基于当前值计算乐观状态并立即展示,pending 结束后自动回落到真实 state(以服务端确认的数据为准),无需手动回滚。典型实践:点赞按钮点击后立即把 count+1、图标变实心,同时发起 server action 提交;提交成功返回的新值驱动 useActionState 更新真实 state,乐观值随之被真实值覆盖;提交失败则在 action 中返回错误,真实 state 保持原值,乐观值自动还原,可配合 useTransition 的 isPending 显示轻微加载态。

工程边界:乐观值只在 Transition 未完成期间生效,若未用 Transition 包裹 setOptimistic,乐观状态不会展示;多次并发乐观更新需要 updateFn 基于当前乐观值累加(如连续点赞),否则后一次覆盖前一次;服务端必须返回权威结果(失败时客户端依赖真实 state 还原),因此乐观更新不适合强一致性场景(如支付、库存扣减),适合"体验优先、可最终一致"的社交/购物场景;购物车场景建议乐观更新数量与总额,但结算金额以服务端为准。

答题主线是"乐观值即时展示 + pending 结束回落真实值"的声明式机制,再用点赞/购物车两个场景讲成功覆盖与失败还原,最后给出并发累加与强一致性场景的边界建议。

const [optLikes, addLike] = useOptimistic(likes, (cur, n) => cur + n);
startTransition(async () => {
  addLike(1);
  await submitLike();
});
#
★★

28. React 19 的 useTransition 在非阻塞状态更新的工程取舍

useTransition 如何实现非阻塞状态更新?在工程中应如何取舍使用?

  • isPending 与 startTransition 的协作
  • 低优先级更新的调度语义
  • 适用的业务场景与不适用场景

useTransition 返回 [isPending, startTransition],startTransition 包裹的 setState 被标记为 Transition 更新,以低优先级调度:不会阻塞紧急输入(打字、点击),可被高优先级更新打断并在空闲时继续,isPending 在过渡进行期间为 true,可据此显示加载态。典型场景:路由切换、大列表过滤、复杂表格刷新等"结果不重要到必须立即呈现"的更新,配合 Suspense 还能在等待期间保留旧 UI 而非直接替换为 fallback。

工程取舍:适合"可延后、可打断"的后台更新;不适合必须即时生效的更新(表单校验错误提示、输入回显),也不适合需要在渲染后立即同步读取 DOM 的代码(Transition 更新是异步的);过度使用会引入无谓的延迟感,若更新本身很快(微秒级),包 Transition 反而增加开销;与 useDeferredValue 二选一:能包函数用 startTransition,值驱动用 useDeferredValue。此外 Transition 期间 isPending 的变化本身也是一次更新,注意避免在渲染中读取 isPending 造成额外渲染循环。

答题核心是"Transition = 低优先级可打断更新 + isPending 状态",再给出适用/不适用场景的二分清单与 useDeferredValue 的选型对比,体现并发特性的工程取舍能力。

#
★★

29. React 19 的 useDeferredValue 在延迟渲染高开销组件的应用

useDeferredValue 如何延迟渲染高开销组件?它的应用模式与注意点是什么?

  • 值延迟与渲染降级的机制
  • 输入即时与结果延后的组合
  • 缓存、memo 与并发打断的注意点

useDeferredValue(value) 返回一个"可能落后于 value"的延迟值:React 用 Transition 优先级渲染依赖该值的部分,紧急更新(输入本身)保持即时,高开销的结果渲染以低优先级在后台完成,期间主线程持续响应输入。经典应用:搜索框 input 的 value 直接用于渲染输入框(紧急),过滤后的结果列表用 deferredValue 渲染(低优先级),用户连续打字时输入不卡顿,结果列表在停顿后更新;若更新尚未完成又被新输入打断,React 会丢弃旧渲染(渲染可中断)。

注意点:依赖 deferred 值渲染的高开销组件应配合 memo/React Compiler 防止无关重渲染,否则每次输入仍会触发整棵子树重渲染,优化失效;deferred 值与当前值不同步,不能在渲染中把两者混用(如用 deferred 做查询参数、同时用当前值做回显会导致不一致);延迟值只在"value 变化"时更新,不会凭空延迟首帧;与 useMemo 配合时注意 useMemo 的依赖应基于 deferredValue 才能缓存结果。

答题抓住"输入紧急渲染 + 派生结果低优渲染"的双轨模式,强调 memo 配合防止重渲染与"延迟值不同步、勿混用"两个注意点,即可覆盖应用与边界。

const q = useState('')[0];
const deferredQ = useDeferredValue(q);
const list = useMemo(() => filter(items, deferredQ), [deferredQ]);
#
★★

30. React 19 的 useSyncExternalStore 在外部数据源订阅的工程应用

useSyncExternalStore 的作用是什么?它在外部数据源(状态库、浏览器 API)订阅中的工程应用与撕裂(tearing)问题如何解决?

  • subscribe/getSnapshot 的 API 契约
  • 撕裂问题与并发渲染的一致性
  • 在状态库与浏览器 API 中的应用

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) 让 React 组件安全地订阅 React 之外的"外部 store":subscribe 注册回调、store 变化时调用 React 强制重渲染,getSnapshot 返回当前快照,React 用它进行一致性比较。它解决了并发渲染下的撕裂(tearing)问题:并发渲染可中断,若组件在渲染中途读取外部 store 而 store 已变化,不同组件可能读到不同版本的数据(撕裂);useSyncExternalStore 在渲染期间缓存快照并强制 store 变化时以高优先级重新渲染,保证同一渲染批次读取一致、且 store 更新不丢失。

工程应用:状态管理库(Zustand、Redux、Jotai 等)内部用它连接 React 与外部状态;浏览器 API 订阅(localStorage、在线状态 navigator.onLine、窗口尺寸、媒体查询)也常通过它封装,其中 getServerSnapshot 提供 SSR 初始值避免水合不匹配;自定义 store 需保证 getSnapshot 返回稳定引用(不可变数据或缓存),否则无限重渲染。React 19 中它也是同步外部状态的标准接口,配合 use() 可读取异步外部源。

答题核心是"订阅 + 快照 + 并发一致性":先讲 API 契约,再重点解释撕裂问题的成因与 useSyncExternalStore 的解决方案(快照缓存与强制同步),最后给状态库与浏览器 API 两个落地场景。

#
★★

31. React 19 的 JSX 转换(新 JSX Transform)

新 JSX Transform 是什么?它与旧 JSX 转换相比有哪些变化与工程收益?

  • react/jsx-runtime 与 jsx/jsxs 函数
  • 不再强制 import React
  • 编译产物与 bundle 体积优化

新 JSX Transform(React 17 引入、19 沿用)把 JSX 编译目标从 React.createElement 改为 react/jsx-runtime 中的 jsx 函数:编译产物调用 jsx(type, props)(多子节点用 jsxs),并在模块顶部自动引入该函数,源码中不再需要 import React 即可使用 JSX。它修复了旧方案的问题:旧方案每个使用 JSX 的模块必须显式 import React,导致 React 必须作为全局依赖存在、且编译器难以做模块级优化;新方案把 JSX 运行时内联为局部引用,支持 tree-shaking 与按需加载。

工程收益:迁移后代码更简洁(不用 import React)、bundle 体积减小(jsx 函数比 createElement 少一次 children 处理且 dev/prod 分离)、与 React 版本解耦(jsx 运行时随包提供)。配置层面:Babel 使用自动运行时(runtime: 'automatic')、TypeScript 设置 jsx: 'react-jsx';若需自定义 JSX pragma 可指定 jsxImportSource。React 19 中继续使用该转换,并且 JSX 本身无破坏性变化,只影响编译产物形态。

答题主线是"新转换 = jsx/jsxs 自动引入 + 无需 import React",再对比旧 createElement 方案说明 bundle 与配置收益,最后给 Babel/TS 的配置要点,覆盖机制与实践。

#
★★

32. React 19 的 Fragment、StrictMode、Profiler 在开发调试的工程价值

Fragment、StrictMode、Profiler 三个组件在开发调试中分别有什么工程价值?如何配合使用?

  • StrictMode 的双调用与潜在问题暴露
  • Profiler 的渲染性能度量
  • Fragment 的结构轻量化与三者协作

Fragment 用于避免多余 DOM 节点(分组渲染、表格行等),保证 DOM 结构与样式(flex/grid)不被破坏;StrictMode 是开发环境专用检查器:组件函数、useState 初始化、useMemo/useReducer 的工厂、render 函数、更新函数会被双调用,useEffect/useLayoutEffect 会执行"挂载-清理-再挂载",以此暴露非纯渲染、副作用泄漏、不幂等的订阅等隐患(生产构建中不生效);Profiler 以 onRender 回调或 DevTools 火焰图展示每次提交的渲染耗时与组件耗时,定位性能瓶颈。

协作价值:StrictMode 暴露的问题大多是并发渲染下的非纯函数问题(渲染期副作用、可变共享状态),先修掉这些隐患再谈优化;Profiler 度量优化前后的提交耗时,验证 memo/useMemo/Compiler 的收益;Fragment 保证优化过程中 DOM 结构不变。实践建议:全应用开启 StrictMode 并在 CI 中关注其警告,Profiler 在性能回归测试中使用(onRender 统计),Fragment 在列表与表格渲染中默认使用。

答题按三个组件的职责分别展开:Fragment 管结构、StrictMode 管正确性(双调用暴露非纯渲染)、Profiler 管性能度量,最后说明"StrictMode 找隐患、Profiler 验证优化"的协作闭环。

#
★★

33. React 19 的 Suspense 在异步组件与数据获取的协作边界

Suspense 如何与异步组件、数据获取协作?其协作边界(缓存、错误、取消)在哪里?

  • Suspense 的挂起-恢复机制
  • 与 RSC、数据获取库的协作
  • 缓存/错误/取消的边界处理

Suspense 把"异步依赖"声明式地接入渲染:组件渲染期间读取未就绪的资源(React.lazy 的加载 promise、RSC 的数据、use(Promise) 读取的 promise、数据获取库的缓存 promise)时挂起,最近的 Suspense 边界渲染 fallback,资源就绪后重新渲染该子树并恢复。与数据获取协作时,最佳实践是"请求在渲染外发起、结果以 promise 缓存"(如 React 19 的 use 配合服务端下发 promise、SWR/React Query 的 suspense 模式、Next.js 的 RSC),这样同一 promise 被多个组件读取不会重复请求,并发渲染中断后 promise 仍有效。

边界:Suspense 要求资源读取是确定性的(同一个 promise 读多次结果一致),否则会陷入"挂起-恢复-再挂起"循环;错误不会渲染 fallback,必须由 Error Boundary 捕获;已挂起的组件不执行 effect,取消场景需在 promise 层处理(AbortController);Suspense 边界过细会导致 fallback 频繁闪烁(可用 startTransition 保留旧 UI)、过粗则等待时间过长,边界位置应在"静态外壳与动态内容"之间。React 19 中 Suspense 也是流式 SSR 的暂停-恢复边界。

答题主线是"渲染期读取异步资源 → 挂起 → fallback → 就绪恢复"的闭环,再展开 promise 缓存(防重复请求)与错误/取消/边界粒度三个实践边界,体现对 Suspense 协作模型的完整认知。

#
★★

34. React 19 的 ErrorBoundary 在组件错误捕获与降级 UI 的工程应用

React 19 中 ErrorBoundary 如何实现组件错误捕获与降级 UI?最佳实践是什么?

  • getDerivedStateFromError 与 componentDidCatch 的分工
  • 降级 UI 与错误恢复(key 重置)
  • 边界粒度与上报策略

ErrorBoundary 是类组件:getDerivedStateFromError(error) 在渲染错误时返回新 state 以切换降级 UI(必须实现,否则错误不可恢复),componentDidCatch(error, info) 用于记录错误与组件栈信息并上报。React 19 中边界支持重置:通过给边界传变化的 key(或 19 新增的 reset 能力)重新挂载子树,实现"错误后重试";同时 19 提供根级 onCaughtError/onUncaughtError 回调统一接管上报,边界自身只需负责 UI 降级。

工程实践:边界粒度按"业务域"划分(路由级、功能模块级),避免全应用一个边界导致局部错误整页白屏,也避免边界过细让降级 UI 泛滥;降级 UI 应提供"重试"操作(重置 key)与错误码,方便用户恢复与反馈;错误上报在 componentDidCatch 或 onCaughtError 中做,注意避免上报本身抛错;React 19 中错误边界与 Suspense 协作:Suspense 恢复失败的错误可由边界捕获。注意边界不能捕获自身错误、事件与异步错误,事件错误用 try/catch 包裹,异步错误用 promise.catch 单独处理。

答题按"两个生命周期方法分工 → 降级与重置 → 边界粒度与上报"三部分组织,突出 React 19 的 key 重置与根级错误回调两个新能力,再强调边界捕获域的局限性。

#
★★

35. React 19 的 Context API 在跨组件状态共享与现代取舍

React 19 的 Context API 如何实现跨组件状态共享?它的性能代价与现代取舍是什么?

  • Provider/useContext 的消费模型
  • context 值变化导致的整树重渲染
  • 与状态管理库、组合模式的取舍

Context API 通过 Provider 在组件树中注入值,任何层级的组件用 useContext 读取,实现了跨层级的"隐式共享",避免 props 逐层透传(prop drilling)。React 19 中 useContext 与 use(Context) 等价,Provider 的 value 变化会让所有直接消费该 context 的组件重新渲染(不管是否 memo 包裹),这是它的主要性能代价:context 是"广播"而非"按需订阅",context 值频繁变化时,消费方整树重渲染。

现代取舍:value 对象每次渲染新引用会导致所有消费者重渲染,需用 useMemo 稳定 value;context 适合"低频变化的共享数据"(主题、语言、当前用户、权限),不适合高频更新的状态(输入内容、列表数据)——后者用组件状态 + props 或状态库;context 与 memo 的协作有边界(memo 不阻断 context 消费),需要 context 拆分成多个小 context(细粒度订阅)治理重渲染;React 19 中 context 也可作为 Server Components 与 Client Components 的桥接(跨边界传递可序列化数据)。取舍原则:共享数据变化频率越低、消费层级越深,context 越合适;变化频繁则用状态库(useSyncExternalStore 驱动的细粒度订阅)。

答题核心是"context 的广播式重渲染代价":先讲共享机制,再讲 value 引用与整树重渲染两个性能点,最后给出"低频共享用 context、高频用状态库/组合"的取舍原则。

#
★★

36. React 19 的 memo 与 useMemo/useCallback 在重渲染优化的边界

React.memo、useMemo、useCallback 在重渲染优化中的边界是什么?React 19 与 React Compiler 时代如何取舍?

  • memo 的 props 浅比较与重渲染条件
  • useMemo/useCallback 的引用稳定性传递
  • Compiler 自动记忆化对手动优化的替代

React.memo 包裹组件后,仅当 props 浅比较(Object.is 逐 key)不相等时才重渲染;useMemo 缓存计算结果、useCallback 缓存函数引用,二者服务于"把稳定引用传给 memo 子组件"以及"避免高开销计算重复执行"。三者的优化边界:memo 只对 props 生效,组件自身的 state/context 变化仍会重渲染;props 中的对象/函数若每次渲染都是新引用(内联写法),memo 形同虚设,必须配合 useMemo/useCallback 或把写法改为稳定引用;useMemo 只缓存不执行,若依赖变化频繁或计算开销小,缓存本身无收益甚至有害(内存占用)。

React 19 与 Compiler 时代:React Compiler 自动进行记忆化(自动 memo、自动 useMemo/useCallback),手工优化逐步让位于编译期推导;但 Compiler 只优化"符合 Rules of React"的代码,第三方组件、无法证明安全的部分仍需手工 memo;React 19 文档也提示 memo 是"最后手段",优先从数据结构与组件拆分(不变 props 提升、细粒度组件)入手。实践中:先量化(Profiler)再优化,避免遍地 useMemo 的"优化洁癖"。

答题按"memo 管 props 浅比较、useMemo/useCallback 管引用稳定性"的分工展开,再强调三者失效边界(context/state、内联新引用),最后落到 React Compiler 自动记忆化对手工优化的替代趋势。

#
★★

37. React 19 的 Portal(createPortal)在模态框与 Tooltip 的工程应用

createPortal 的作用是什么?在模态框与 Tooltip 场景中如何应用,有哪些注意点?

  • Portal 的事件冒泡与 DOM 位置解耦
  • 模态框的焦点管理与会话语义
  • 与 z-index/overflow 裁剪问题的治理

createPortal(children, container) 把 React 子树渲染到 DOM 树的其它位置(如 document.body),但保留 React 树中的父子关系:事件冒泡遵循 React 树而非 DOM 树,context 与 props 传递不受影响。模态框、Tooltip、Dropdown 悬浮层常因祖先的 overflow: hidden/auto、transform、z-index 层叠上下文被裁剪或遮挡,Portal 到 body 后彻底脱离这些约束,配合定位计算实现悬浮效果。

工程注意点:React 树中的冒泡可能导致点击 Portal 内容意外触发祖先的 onClick,需用 stopPropagation 或判断事件来源;模态框需要用焦点陷阱(focus trap)把 Tab 循环限制在弹窗内、打开时聚焦弹窗、关闭时归还焦点到触发元素,并设置 aria-modal/role="dialog" 保证语义;SSR 时 body 尚不存在,Portal 需在 effect 中挂载或客户端检测(避免水合不匹配);多个 Portal 可共用同一容器,React 19 中 portal 与并发特性协作正常,但注意避免在 Transition 中频繁创建/销毁 portal 引起的视觉闪烁。

答题主线是"Portal = DOM 位置与 React 树的解耦(事件冒泡跟 React 树)",再讲模态框的焦点管理与无障碍、SSR 挂载时机两个工程点,最后提层叠上下文治理,覆盖应用与边界。

createPortal(
  <div role="dialog" aria-modal="true">{content}</div>,
  document.body
)
#
★★

38. React 19 的 Lane 优先级模型(SyncLane、InputContinuousLane、TransitionLane)在用户输入响应的边界

SyncLane、InputContinuousLane、TransitionLane 分别代表什么?它们在用户输入响应中的边界如何体现?

  • 三类 lane 的优先级划分与来源
  • 输入响应性与后台更新的抢占
  • 紧急更新与 Transition 的边界实践

Lane 优先级模型中:SyncLane(同步 lane)最高,来自紧急离散事件(点击、键盘按下)、flushSync 与同步渲染,渲染立即执行不可中断;InputContinuousLane(连续输入 lane)次之,来自连续事件(拖拽、滚动、指针移动、连续输入),保证跟手;TransitionLane(过渡 lane)最低,来自 startTransition/useTransition/useDeferredValue 标记的更新,可被前两者打断、按时间切片断续执行。调度器按"优先级从高到低、同优先级按进入顺序"处理,紧急事件始终抢占低优先级渲染。

用户输入响应的边界:打字、点击等离散输入必须走紧急路径(默认 setState 即是 SyncLane),保证毫秒级响应;拖拽/滚动等连续输入走 InputContinuousLane,React 会优先完成这些更新;批量后台任务(过滤大列表、数据同步)应显式用 Transition 降级,避免阻塞输入;同一事件处理器内混合紧急与过渡更新时,紧急更新先渲染、过渡更新可被随后的事件打断。工程上把"用户体验关键路径"保持在紧急 lane,"结果可稍后呈现"的工作降级到 Transition lane,是并发模式下响应性能的黄金法则。

答题先厘清三类 lane 的触发来源与优先级序(Sync > InputContinuous > Transition),再落到"紧急输入即时渲染、后台更新可打断"的响应性边界,最后给事件类型的 lane 归属实践。

#
★★

39. React Fiber 的 beginWork/completeWork 阶段在函数组件与类组件的统一协调路径

beginWork/completeWork 如何统一协调函数组件与类组件?两条路径的差异体现在哪里?

  • 两类组件的 beginWork 入口与处理差异
  • hooks 与生命周期在协调中的位置
  • 统一协调路径的意义

React 协调器对函数组件与类组件走同一条 Fiber 遍历路径:beginWork 按 tag(FunctionComponent/ClassComponent)分派到不同处理函数,但都完成"读取 props/state → 判断是否 bailout(旧 props 与新 props 相同则跳过子树)→ 计算新的子元素 → 通过 reconcileChildren 生成子 Fiber"的统一流程;completeWork 对两类组件都只做"创建/更新 DOM 节点、收集副作用、上传 subtreeFlags",函数组件本身不产生 DOM(由后代决定)。差异体现在 beginWork 内部:函数组件调用函数体执行 Hooks(hooks 链表挂在 Fiber 的 memoizedState 上),类组件实例化并调用 render 方法、更新 refs;useState 与 this.setState 最终都汇入 update queue,经 same lane 调度进入同一 render-commit 管线。

统一路径的意义:保证两类组件在并发渲染、优先级调度、错误恢复(Error Boundary)、Suspense 挂起等机制下行为一致——如挂起时两者都以"抛异常"方式中断并恢复,批处理对两者等效;React 19 中函数组件路径更受新特性倾斜(React Compiler、use 等仅支持函数组件),但协调内核保持统一,Class 组件仍可稳定运行。

答题抓住"统一 Fiber 遍历 + tag 分派"的框架:两类组件在 beginWork 的差异(hooks vs 生命周期)不影响 completeWork 的收尾统一,再点出统一路径对并发/错误/Suspense 一致性的保障。

#
★★

40. React 19 的 useTransition 与 useDeferredValue 在过滤大列表时让出主线程的现代边界

useTransition 与 useDeferredValue 在过滤大列表时如何让出主线程?现代 React 中有什么边界与更优方案?

  • 双轨渲染与时间切片让出主线程
  • 与 useMemo/Compiler 的组合
  • 虚拟列表等更优方案

过滤大列表时,输入本身走紧急更新保持即时,过滤计算与列表渲染包进 Transition(startTransition/useTransition)或使用 useDeferredValue 延迟值:React 把这些工作标记为低优先级,按 5ms 时间片断续执行,每片之间让出主线程处理输入事件,用户连续打字时列表过滤"总是被新输入打断",输入框不卡顿。useDeferredValue 适合"值驱动"的过滤(值变化即重算),useTransition 适合"事件驱动"(在事件中明确划分紧急与过渡)。

现代边界:Transition 只解决"让出主线程",不解决"渲染规模大"本身——过滤本身的计算与整棵列表 DOM 的渲染依然耗时,若列表上万项,切片后总时长不变,只是不阻塞;应叠加 useMemo 缓存过滤结果(依赖 deferredValue)、React Compiler 自动记忆化、以及虚拟列表(只渲染可视区)从规模上治理;Suspense + Transition 可保留旧列表 UI 直到新结果就绪,避免 fallback 闪烁。另外要注意 useDeferredValue 在 React 19.1+ 提供 compare 参数,可减少无意义的重算。

答题主线是"紧急输入 + 低优过滤的时间切片协作",再点明 Transition 不缩小渲染规模这一关键边界,并给出 useMemo/虚拟列表/Compiler 的组合方案,体现性能治理的层次。

#
★★

41. React 的 Suspense for Data Fetching 在并发渲染下 ping 与 boundary 的工程实践

Suspense for Data Fetching 中"ping"机制是什么?并发渲染下 Suspense 边界如何工作,工程实践有哪些?

  • 数据就绪后触发重渲染的 ping 机制
  • 挂起组件的边界恢复与缓存 promise
  • 避免瀑布流与 fallback 闪烁的实践

Suspense for Data Fetching 中,组件读取未就绪的数据时抛出一个"挂起信号"(通常是 promise),最近的 Suspense 边界捕获后渲染 fallback;数据请求完成时,资源层(缓存 promise)调用"ping"——向 React 调度器发信号,提示"有组件等待的数据已就绪",React 以合适的优先级重新渲染该边界内的子树,组件再次读取缓存时拿到数据并渲染真实内容。ping 保证了"无需组件手动 setState"的恢复路径,且与并发渲染兼容:渲染中断后 promise 仍在缓存中,恢复读取不会重复发起请求。

工程实践:数据获取应发生在渲染外(useEffect、RSC、数据库的 suspense 模式)并把 promise 缓存起来,避免每次渲染重新发起;并发渲染下 ping 触发的恢复渲染可能被高优先级输入打断,恢复延迟属预期;用 startTransition 包裹"切到新数据"的更新可保留旧 UI(避免 fallback 闪烁);边界粒度:每个独立数据块一个边界,避免一个慢接口阻塞整页;错误用 Error Boundary,不渲染 fallback。React 19 中 RSC 与 use() 把该模式内置化:服务器把 promise 序列化进 Flight payload,客户端 use() 直接读取。

答题核心是"挂起抛 promise + 就绪后 ping 触发恢复渲染"的闭环,再讲 promise 缓存、Transition 保留旧 UI、边界粒度三个工程点,并提 RSC/use 的现代演进。

#
★★

42. React 19 的 use() Hook 直接读取 Promise/Context 在并发模式下的边界

React 19 的 use() 直接读取 Promise 与 Context 的边界是什么?并发模式下有哪些注意点?

  • use() 的挂起语义与条件调用
  • Promise 缓存与并发安全
  • 与 Hooks 规则的边界差异

use(promise) 在渲染期间读取 promise:未 resolve 时挂起(由最近的 Suspense 边界展示 fallback),resolve 后重新渲染返回结果;use(context) 读取 context 值。与 Hooks 的最大边界差异是 use() 不要求"顶层、无条件、固定顺序"调用——它可以在条件分支、循环中调用,因为其语义只依赖被读取的值而非调用顺序;但其它 Hooks 的规则依然适用(use() 不受 Hook 顺序约束不代表可以乱用其它 Hook)。并发模式下 use() 读取的 promise 必须是稳定的(缓存的 promise 对象),若每次渲染 new Promise,React 无法区分"新请求"与"同一请求",会陷入无限挂起循环。

其它边界:use() 不能用于事件处理与 effect(非渲染代码),挂起信号不能被 try/catch 捕获;use(context) 变化时组件重渲染,与 useContext 行为一致;在 RSC 场景,客户端 use() 可消费服务端下发的 Flight promise,这是 RSC 数据流的关键接入点;React 19 文档强调 use() 是"渲染期专用",读取的资源应来自缓存层(如请求缓存、store),保证跨渲染确定性。

答题先讲 use() 的挂起/恢复语义,再突出"可在条件中调用 ≠ 可乱用其它 Hook"与"promise 必须缓存稳定"两个并发边界,最后落到 RSC 数据流接入点。

#
★★

43. React 的 useSyncExternalStore 在外部可变源订阅与并发渲染撕裂(tearing)

useSyncExternalStore 如何解决外部可变源订阅在并发渲染下的撕裂(tearing)问题?实现机制是什么?

  • 撕裂问题的成因(渲染中断 + 外部源变化)
  • 快照缓存与强制同步重渲染
  • 稳定快照引用的要求

撕裂指并发渲染下 UI 显示不一致的数据版本:渲染可中断,若一个组件在中断前读到 store 的旧版本、另一个组件在恢复后读到新版本,同一帧内就会出现"数据撕裂"。useSyncExternalStore 通过两条机制解决:一是渲染期间缓存快照——render 阶段读取外部 store 时把快照存在 fiber 上,同一渲染内后续读取复用,保证单次渲染一致;二是强制同步——store 变化(触发 subscribe 回调)时,React 以高优先级(SyncLane)重新渲染,让"读旧快照"的组件迅速收敛到新版本,避免跨渲染读旧数据。

实现要点:getSnapshot 必须返回稳定引用(不可变数据),若每次返回新对象,React 会认为"值变了"触发无限重渲染循环;subscribe 回调在 store 变化时调用,React 内部用"检测到变化 → 调度重渲染"的方式接管;SSR 用 getServerSnapshot 提供初始值保证水合一致。工程应用:状态库(Zustand/Redux/Jotai 内部)、浏览器 API(online 状态、媒体查询、localStorage 跨标签页)的 React 化封装都依赖它。

答题先解释撕裂成因(可中断渲染 + 外部源变化的时间差),再讲两条解法(快照缓存 + 强制同步重渲染),最后给稳定引用与 SSR 快照两个实现要点,构成完整机制链。

#
★★

44. React 19 的 ref 清理函数(ref={(node) => { ...; return cleanup }})与 useEffect 清理的边界

React 19 的 ref 回调清理函数与 useEffect 清理函数有什么边界差异?分别在什么场景使用?

  • ref 回调返回清理函数的时机
  • ref 清理与 effect 清理的执行顺序
  • 命令式 DOM 管理与副作用的职责划分

React 19 中 ref 回调可以返回清理函数:ref={(node) => { attach(node); return () => detach(node); }},在节点卸载或 ref 变化(新回调替换旧回调、ref 变为 null)时执行清理,用于命令式资源释放(解绑观察器、移除监听、清理第三方实例)。与 useEffect 清理的边界:ref 清理与 DOM 生命周期绑定更紧密——发生在卸载该节点时(含被删除、被替换、函数组件 ref 重挂载),且在同一个 commit 内先于 useEffect 清理执行(mutation 阶段清理 ref、layout/passive 阶段处理 effect),因此适合"必须与 DOM 存在性同步"的资源;useEffect 清理则更通用,适合订阅、计时器、请求取消等与渲染周期对齐的副作用。

边界注意:ref 回调在每次渲染(若回调引用变化)或节点重建时都会以 null 再以 node 调用,清理函数必须幂等(重复清理安全);不要在 ref 回调中执行 setState(会触发额外渲染循环);React 19 中函数组件 ref 作为 prop 传递后,父组件可通过 ref prop 给子组件传入带清理的回调;职责划分原则:DOM 直接相关、须随节点销毁释放的用 ref 清理,渲染周期相关、须随 effect 生命周期管理的用 effect 清理。

答题核心是"ref 清理绑定 DOM 节点生命周期、先于 effect 清理执行",再给幂等性与 setState 禁令两个边界,最后按"DOM 相关 vs 渲染周期相关"划分职责。

<div ref={(node) => {
  if (!node) return;
  const obs = new ResizeObserver(() => {});
  obs.observe(node);
  return () => obs.disconnect();
}} />
#
★★

45. React Scheduler(调度器)的 shouldYield() 在 5ms 时间片到期的判断逻辑

Scheduler 的 shouldYield() 如何判断 5ms 时间片到期?超时后的处理逻辑是什么?

  • 时间片的起点与到期计算
  • shouldYield 的返回值语义
  • 超时后的任务让出与重新调度

Scheduler 用"开始时间戳 + 时间片长度"判断是否到期:进入渲染任务时记录 startTime(用 performance.now(),无则 Date.now()),每次执行渲染循环前调用 shouldYield(),比较 performance.now() - startTime >= 5ms(时间片默认 5ms,可通过 frameInterval 配置)是否成立:成立则返回 true,表示"时间片已耗尽、应让出主线程"。让出后 React 把剩余渲染工作以新的调度任务(通过 MessageChannel 投递宏任务)放回队列,下一轮循环重新记录 startTime,继续渲染,从而把长渲染切分为多个 ~5ms 的片,片间主线程可处理输入、绘制等紧急工作。

判断逻辑的细节:timeout 与过期任务不同——低优先级任务有独立过期时间,过期后会被"同步强制执行"(避免饿死),shouldYield 只控制"是否让出",不负责过期策略;MessageChannel 比 setTimeout 更早触发且不受最小延迟钳制,保证切片间隔稳定;若连续多个任务都到期让出,调度器会调整 frameInterval 或在空转时延长片长(yield 后重新计算),平衡吞吐与响应。理解 shouldYield 是理解 React 时间切片与并发渲染主线程协作的关键。

答题主线是"startTime + 5ms 比较 → 到期返回 true → 任务放回队列继续渲染"的切片循环,再补充 MessageChannel 投递与过期任务强制执行两个细节,形成完整机制认知。

#

46. React 19 的受控与非受控组件(refs)在表单状态的取舍

React 19 中受控与非受控组件在表单状态管理上如何取舍?refs 在其中扮演什么角色?

  • 受控/非受控的状态归属
  • ref 读取与 defaultValue 的配合
  • React 19 ref-as-prop 的简化

React 19 中受控组件用 state 持有表单值、onChange 同步更新,适合联动校验、条件渲染与提交前处理;非受控组件用 defaultValue 设置初始值、值由 DOM 持有,需要时用 ref 读取(inputRef.current.value),适合性能敏感、简单独立或与第三方库协作的字段。React 19 的重要变化是 ref 可以直接作为 prop 传给函数组件(无需 forwardRef),让非受控字段的"实例读取"更简洁:父组件把 ref 当普通 prop 传下,子组件挂到 DOM 节点上。

取舍建议:混合使用是常态——react-hook-form 默认非受控 + ref 注册,配合 Controller 转受控处理联动字段;受控适合"值决定渲染"(实时预览、按钮禁用态),非受控适合"提交时读值"(不参与渲染的字段);React 19 的 form action 把提交统一为"读 FormData"(非受控友好),受控字段需手动同步进 FormData。边界:非受控字段的校验提示若不触发渲染则不会自动更新 UI,需额外 setState;受控字段高频输入有渲染开销。原则:按"值是否参与渲染决策"选型。

答题以"值参与渲染则受控、提交时才读则非受控"为选型主线,突出 React 19 的 ref-as-prop 简化与 form action 的 FormData 读取方式,最后给混合使用建议。

#

47. React 19.2 的 与 Suspense fallback 在隐藏内容、保留 state、处理 Effect 和后台更新方面有什么本质差异

React 19.2 的 与 Suspense fallback 在隐藏内容、保留 state、处理 Effect 和后台更新方面有什么本质差异?

  • Activity 保留 state 与 Suspense 替换 UI 的差异
  • Effect 在两种模式下的执行策略
  • 后台更新的暂停与恢复

(Offscreen 的正式化)把子树隐藏但保留其在 React 树中的挂载状态:DOM 用 display:none 隐藏、组件的 state/ref/滚动位置保留,切回 visible 时无需重新挂载;Suspense fallback 则是"替换"语义——挂起时渲染 fallback,原内容被卸载(组件卸载、state 丢失),恢复时重新渲染原内容。因此 Activity 适合"保活"场景(Tab 工作台、路由缓存、Modal 底层面板),Suspense 适合"等待资源就绪"场景(lazy 加载、数据获取),两者语义完全不同:一个隐藏、一个替换。

Effect 与后台更新:Activity 隐藏期间,子树默认停止渲染更新,React 19.2 起隐藏树中的 Effect 默认不执行(可配置),避免后台页面持续消耗资源;Suspense fallback 显示期间原组件已被卸载,其 effect 已清理,不存在"后台执行"问题。Activity 在隐藏状态下仍可接收高优先级更新(如保留输入焦点时的紧急更新),普通更新被推迟到可见后应用;后台更新的处理是可配置的(默认暂停)。差异本质:Activity 是"同一棵树的可见性切换",Suspense 是"两棵树的切换(等待态 vs 就绪态)"。

答题抓住"隐藏 vs 替换"的本质差异:Activity 保留挂载与 state、Suspense 卸载并重挂载,再分别讲 Effect 策略(隐藏暂停 vs 卸载清理)与后台更新(推迟 vs 不存在),形成对照式答案。

#

48. 将路由页和多 Tab 工作台改为 保活时,如何决定哪些页面应隐藏而不是卸载,并用切换延迟、后台 CPU、订阅数量和堆内存上限验证收益没有被保活成本抵消

将路由页和多 Tab 工作台改为 保活时,如何决定哪些页面应隐藏而不是卸载?如何量化验证保活收益没有被成本抵消?

  • 保活页面的选择标准(状态价值 vs 资源成本)
  • 切换延迟与后台 CPU 的度量
  • 订阅数量与堆内存上限的监控

决定"隐藏还是卸载"的标准是"页面状态的保留价值 vs 保活成本":适合隐藏的页面——保留重要交互状态(表单草稿、滚动位置、筛选条件、图表配置)且这些状态重建成本高、用户高频往返(Tab 切换、返回上级路由);适合卸载的页面——状态无保留价值(一次性详情页)、体积大但状态少、或包含高耗资源(长时间定时器、高频轮询、WebSocket、重动画)的页面,保活只会放大常驻成本。工程上先列候选页面的"状态清单 + 重建成本 + 资源占用",再按收益排序,避免一刀切全保活。

验证方法:切换延迟——用 Performance API/User Timing 记录切回页面到可交互的时间,对比保活前后(预期大幅下降);后台 CPU——用 PerformanceObserver('longtask') 与 DevTools Performance 面板度量隐藏页面在后台的 long task 频率与耗时(Activity 暂停渲染后应接近 0,若仍有高频 activity 说明效果/订阅未停);订阅数量——给轮询、WebSocket、observer 加计数埋点,统计隐藏期间存活订阅数;堆内存上限——用 performance.memory 或 DevTools 堆快照对比"保活 N 个页面"与"卸载"的内存增长曲线,设定上限(如超出卸载基线 X% 或绝对阈值),超限则对该页面改为卸载。最终以"切换延迟下降 vs CPU/内存上升"的净收益决定每页策略。

本题考察性能工程的量化思维:先给选型标准(状态价值 vs 资源成本),再给出四个可度量指标(延迟、CPU、订阅数、内存)与工具,强调"保活收益必须大于成本"的验证闭环。

#

49. React 的 ErrorBoundary 与 Suspense 的 fallback 边界在并发模式的协作

ErrorBoundary 与 Suspense 的 fallback 在并发模式下如何协作?边界划分是什么?

  • 错误与挂起的处理分工
  • 恢复失败时的错误接管
  • 边界嵌套与优先级协作

Suspense 处理"未就绪"(挂起、等待),ErrorBoundary 处理"已失败"(错误),二者在并发模式下协作:组件挂起时由最近的 Suspense 边界渲染 fallback;若挂起的资源最终失败(promise reject)或 fallback 自身渲染抛错,错误沿 React 树向上传播,由最近的 ErrorBoundary 捕获并渲染错误 UI。分工边界:Suspense 不处理错误(promise reject 不会显示 fallback 的错误态)、ErrorBoundary 不处理挂起(错误边界不能"等待"),错误边界位于 Suspense 外层时可兜底其子树(含 fallback 渲染错误)的所有失败。

并发模式协作要点:Suspense 恢复渲染被高优先级打断后,错误边界不受影响(错误状态一经确定即提交);startTransition 包裹的更新中若 Suspense 重新挂起,React 保留旧 UI 而非显示 fallback(过渡期不闪烁),此时错误仍由边界处理;嵌套边界遵循"最近者接管"原则:内层边界存在时错误不冒泡到外层;React 19 中 onRecoverableError 处理 React 自行恢复的错误(如水合错误),与边界协作形成"边界兜底 + 根级回调上报"的完整错误治理。

答题先划分"Suspense 管等待、ErrorBoundary 管失败"的职责,再讲恢复失败时错误的向上传播与边界嵌套的"最近者接管",最后提 Transition 保留旧 UI 与根级回调,覆盖协作全貌。

#

50. React 的 startTransition 与 useTransition 在用户输入响应的优先级协作

startTransition 与 useTransition 在用户输入响应中如何协作?优先级是如何安排的?

  • 两个 API 的等价性与形态差异
  • 紧急输入与过渡更新的优先级协作
  • isPending 驱动的 UI 反馈

startTransition(fn) 与 useTransition 返回的 startTransition 等价,都是把 fn 内的更新标记为 Transition 低优先级;区别仅是 useTransition 额外返回 isPending 供 UI 读取。用户输入响应中,紧急更新(输入框 value、点击反馈)保持默认高优先级(SyncLane)立即渲染,而包裹在 startTransition 中的更新(过滤结果、路由切换内容、大列表刷新)降为 TransitionLane:可被后续输入打断、按时间片执行,从而"输入永远先于后台渲染完成"。二者协作模式:事件处理器中先 setState 更新紧急部分,再把重活包进 startTransition;或输入值走紧急更新、消费值用 useDeferredValue 延迟(声明式等价物)。

优先级协作细节:Transition 内部再产生的新更新(如过渡期间用户又输入)会以更高优先级打断旧过渡,React 会丢弃不完整的中断渲染;isPending 在过渡开始到结束期间为 true,可显示轻量 loading 或禁用按钮(但避免用 isPending 直接阻塞输入);Suspense + startTransition 协作时,过渡期间即使组件挂起也保留旧 UI(不闪 fallback)。边界:startTransition 内的同步代码立即执行,只有其中的 setState 被降级;紧急更新不可放入 Transition(如必须即时响应的校验)。

答题主线是"紧急更新高优即时渲染 + Transition 低优可打断"的协作分工,再讲 isPending 的 UI 反馈与 Suspense 保留旧 UI 两个协作细节,最后强调 setState 才是降级对象。

#

51. React 19 的 组件封装(并非独立的 useViewTransition Hook)与浏览器 View Transitions API 的工程协作

React 19 的 组件封装与浏览器 View Transitions API 如何工程协作?为什么它不是 useViewTransition Hook?

  • 组件封装与 Hook 封装的区别
  • startViewTransition 的回调时序
  • 过渡兼容性与降级

React 19 用组件 而非 Hook 封装 View Transitions:因为过渡需要"包住整棵要过渡的子树",组件式声明能自然表达"该区域内的更新在过渡中应用",而 Hook 只能返回回调、无法声明式界定渲染区域。其实现:组件挂载后把内部更新包装进 document.startViewTransition 的回调,React 在回调中执行更新并返回 promise,浏览器截取旧快照、DOM 更新后截取新快照、播放过渡动画;React 在过渡结束后继续正常渲染生命周期。

工程协作要点:过渡回调内的更新必须是"一次性整块"的(大范围 DOM 变化,如路由切换、主题切换),高频细粒度更新会导致快照频繁无效;startViewTransition 需要浏览器支持(Chromium),React 内部会特性检测,不支持时降级为普通更新,无需开发者处理;动画样式通过 ::view-transition-old/new 伪元素自定义,React 不干预;配合 prefers-reduced-motion 关闭动画以尊重用户偏好;与 startTransition(优先级)组合使用时, 管视觉过渡、startTransition 管更新优先级,两者职责正交。

答题先解释"为何组件而非 Hook"(声明式界定渲染区域),再讲组件与 startViewTransition 的包装时序(旧快照 → 更新 → 新快照 → 动画),最后给降级与 reduced-motion 两个工程点。

#

52. React Fiber 的 subtreeFlags 与 deletions 数组在 commit 阶段副作用执行顺序的工程价值

React Fiber 的 subtreeFlags 与 deletions 数组在 commit 阶段如何保证副作用执行顺序?工程价值是什么?

  • subtreeFlags 位掩码遍历机制
  • deletions 数组的删除收集
  • 副作用执行顺序的确定性

subtreeFlags 是 Fiber 上的位掩码,标记"子树中是否包含某类副作用"(如 Placement、Update、Passive、Ref 等),completeWork 向上合并:父节点 subtreeFlags |= 子节点 flags | subtreeFlags。commit 阶段遍历时先检查 subtreeFlags:若子树不含目标副作用则整棵跳过,含则下钻并按深度优先顺序执行,替代了旧版 effect list 链表的维护,在保证"按树结构确定的执行顺序"的同时减少遍历开销。deletions 数组挂在父 Fiber 上,收集本次更新中被删除的子 Fiber,commit 的 mutation 阶段统一执行删除(卸载组件、调用 componentWillUnmount/effect 清理、移除 ref 与 DOM),避免边遍历边删导致的顺序混乱。

工程价值:顺序确定性是 React 保证副作用可预期的基石——mutation 阶段先处理删除再处理插入更新(删除按 children 顺序、插入按树序),layout 副作用在 mutation 后同步执行,passive(useEffect)在提交后统一异步执行;subtreeFlags 的批量跳过让 React 19 在大型应用中显著减少 commit 遍历成本(无需逐个节点检查)。理解这套机制能解释"为何 useEffect 清理一定先于下一次 effect 执行""为何删除的子树的清理在父节点卸载前完成"等工程现象。

答题主线是"subtreeFlags 按位标记 + 深度优先跳过遍历 + deletions 统一删除",强调两者共同保证 commit 阶段副作用顺序的确定性与性能,最后落到可预期的工程现象。