跳到正文
前端知识库
React

React 面试真题补充:渲染、状态与工程实践

整理 React 面试资料中的 JSX、事件系统、Hooks、Fiber、性能优化、路由、Redux、不可变数据和 SSR 要点。

11 分钟React · Hooks · Fiber · Redux · SSR · 面试

React 面试真题补充:渲染、状态与工程实践

本文根据编号 4《React 面试真题》整理,补充项目已有 React 文章中偏工程场景和面试追问的部分。

1. JSX、组件与渲染

JSX 不是浏览器原生语法,构建工具会把它转换成 createElement 调用或等价的 JSX runtime 调用:

const element = (
  <section className="card">
    <h2>{title}</h2>
    <Button onClick={onSelect}>查看</Button>
  </section>
)

渲染函数应保持纯净:相同的 props、state 和 context 应产生相同的 React 元素,不应在 render 阶段发请求、修改外部变量或直接操作 DOM。React 会先生成新的元素树,再协调到宿主环境;当前 React 应用通常通过 createRoot 挂载,资料中的 ReactDOM.render 属于旧版本 API。

函数组件和类组件都能表达 UI,但新代码通常优先函数组件 + Hooks。类组件仍会出现在旧项目、错误边界和历史代码中,面试时应能读懂两种写法。

2. Props、State 与受控组件

Props 是父组件传入的只读输入,state 是组件自己拥有并会影响渲染的数据。子组件需要改变父级数据时,应通过回调表达意图:

function SearchBox({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={(event) => onChange(event.target.value)}
    />
  )
}

这类输入框是受控组件,真实值来自 React state。非受控组件把值交给 DOM,通过 defaultValue 和 ref 读取:

function FileNameForm() {
  const inputRef = useRef(null)

  function submit(event) {
    event.preventDefault()
    console.log(inputRef.current?.value)
  }

  return (
    <form onSubmit={submit}>
      <input ref={inputRef} defaultValue="draft" />
      <button type="submit">提交</button>
    </form>
  )
}

大量表单字段、需要即时校验和联动时受控模式更容易统一建模;简单表单或第三方 DOM 控件可以考虑非受控模式。不要把 props 无条件复制到 state,否则会产生两个事实来源。

3. 类组件中的 this 与事件系统

类方法作为回调传递时会丢失实例上下文,常见处理有三种:

class Counter extends React.Component {
  state = { count: 0 }

  handleClick = () => {
    this.setState((state) => ({ count: state.count + 1 }))
  }

  render() {
    return <button onClick={this.handleClick}>{this.state.count}</button>
  }
}

也可以在构造函数中 bind,或在 JSX 中使用 onClick={() => this.handleClick()}。内联函数最直观,但在高频列表中要注意引用稳定性和子组件是否因此重复渲染。

React 的事件处理器使用统一的合成事件接口,并在 React 根容器附近做事件委托。event.currentTarget 表示当前处理器绑定的元素,event.target 表示真正触发事件的元素;需要访问底层浏览器事件时使用 event.nativeEvent。不要把 React 的 onClick 与原生 onclick 的大小写和绑定方式混用。

4. Hooks 与 Effect 生命周期

useState 保存会影响渲染的状态,下一状态依赖旧状态时使用函数式更新:

const [count, setCount] = useState(0)

function incrementTwice() {
  setCount((current) => current + 1)
  setCount((current) => current + 1)
}

useEffect 用于同步外部系统,例如订阅、定时器、网络请求或文档标题。返回清理函数,并让依赖数组完整描述闭包使用的值:

useEffect(() => {
  const controller = new AbortController()

  loadUser(userId, controller.signal).then(setUser)
  return () => controller.abort()
}, [userId])

空依赖数组表示只依赖初始渲染捕获的值,并不等于“组件永远只执行一次”:开发模式 Strict Mode 可能先执行一次 setup 和 cleanup 来发现副作用问题。需要在浏览器绘制前同步读取或调整布局时才考虑 useLayoutEffect,否则优先使用 useEffect

5. setState、批量更新与闭包

React 可能批量处理同一事件或异步回调中的状态更新。直接使用闭包中的旧值可能丢更新:

// 可能只增加 1
setCount(count + 1)
setCount(count + 1)

// 明确基于最新状态增加 2
setCount((current) => current + 1)
setCount((current) => current + 1)

类组件中的 setState 也支持对象或函数,并可以通过回调读取提交后的状态。不要把“异步”理解为固定的 Promise 延迟;关键是 React 会根据更新优先级和批处理时机安排渲染。

6. Context、Refs 与高阶组件

Context 适合主题、语言、登录用户等跨层稳定依赖,不适合把所有业务状态都塞进一个 Provider。Provider 的 value 引用变化会让消费者重新评估,因此可以拆分 context 或稳定 value:

const value = useMemo(() => ({ user, logout }), [user, logout])
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>

Refs 用于聚焦、测量、媒体控制和接入第三方 DOM 库,不应作为普通状态通道。函数组件对外暴露命令式方法时,可以组合 forwardRefuseImperativeHandle;不要为了绕过 props 数据流而滥用 ref。

高阶组件(HOC)是接收组件并返回增强组件的函数。编写 HOC 时要透传未消费的 props、设置有意义的 displayName,并注意 ref 不会自动透传。Hooks 在新代码中通常能以更局部、更易组合的方式替代 HOC,但旧项目仍常见 connect、权限包装和埋点包装。

7. 错误边界

错误边界用于捕获子树渲染、生命周期和构造阶段的错误,并显示降级 UI:

class ErrorBoundary extends React.Component {
  state = { hasError: false }

  static getDerivedStateFromError() {
    return { hasError: true }
  }

  componentDidCatch(error, info) {
    reportError(error, info)
  }

  render() {
    return this.state.hasError ? <Fallback /> : this.props.children
  }
}

错误边界不会自动捕获事件处理器、异步回调或服务端渲染阶段的所有错误,这些场景仍需在事件和 Promise 链路中显式处理。错误日志要附带路由、用户操作和构建版本,便于定位。

8. Virtual DOM、Diff 与 Fiber

React 的协调过程可以拆成:

组件执行 -> React 元素树 -> Fiber 协调 -> 提交 DOM 副作用

Fiber 节点保存组件类型、key、父子兄弟关系、待处理 props/state 和副作用标记。它把大段递归工作拆成可调度的工作单元,使渲染阶段有机会暂停、恢复和按优先级处理;提交阶段则集中执行 DOM 变更。

列表 Diff 依赖稳定 key 判断节点身份:

  • key 只需在同一层兄弟节点中唯一。
  • 数据 ID 比数组索引更可靠,尤其是插入、删除、排序时。
  • key 改变会让 React 认为是新节点,从而重置该子树状态。
  • 不要把随机值用作 key,否则每次渲染都会销毁并重建列表项。

不同类型的元素通常会替换对应子树;同类型节点则继续比较 props 和子节点。React 不承诺任意跨层移动都能被低成本复用,组件树应保持稳定层级。

9. 性能优化的判断顺序

先确认是否真的存在重复渲染或长任务,再选择工具:

  1. 缩小状态作用域,避免把局部输入状态放到过高层级。
  2. 用稳定的 key 和合理的组件边界减少无意义卸载。
  3. React.memoPureComponentshouldComponentUpdate 跳过 props/state 未变化的子树。
  4. 只有计算昂贵或引用稳定性确实影响下游时,才使用 useMemo / useCallback
  5. 对大列表使用虚拟化,对低优先级更新考虑并发 API。

React.memo 默认做浅比较;传入每次创建的新对象、数组或函数仍可能导致子组件重新渲染。记忆化不是越多越好,它本身也有比较和维护成本。

10. 懒加载与 Suspense

路由页面和大型功能可以按边界拆分:

const Settings = React.lazy(() => import('./Settings'))

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Settings />
    </Suspense>
  )
}

lazy 返回的组件在加载完成前会挂起,最近的 Suspense 提供占位 UI。加载边界应尽量贴近用户感知的功能区,避免整个应用只显示一个大范围 loading;网络失败还需要错误边界提供重试入口。

11. Router 的模式与权限

BrowserRouter 使用 History API,URL 更自然,但生产服务器需要把未知路径回退到入口 HTML;HashRouter 把路径放在 # 后,不依赖服务端重写,但 URL 和 SEO 能力较弱。

路由参数、查询参数和 location state 的职责不同:

  • /detail/:idid 是资源身份,适合可分享链接。
  • ?page=2 适合筛选、分页等可复制状态。
  • location.state 适合临时导航上下文,刷新后不应依赖它仍存在。

权限路由应在渲染受保护页面前完成身份和角色判断,避免先展示敏感内容再跳转。动态路由、菜单和按钮权限要使用同一份权限模型,不能只隐藏前端按钮而忽略服务端授权。

12. Redux 数据流与 Middleware

Redux 的基本链路是:

dispatch(action) -> reducer(state, action) -> new state -> selector -> UI

Reducer 必须是纯函数,不能直接修改旧 state。Provider 提供 store,connect 或 hooks 订阅所需片段;选择器应尽量返回稳定引用,避免无关更新触发渲染。

Middleware 位于 dispatch 和 reducer 之间,可以记录 action、处理异步函数或统一错误:

const logger = (store) => (next) => (action) => {
  console.log('before', store.getState())
  const result = next(action)
  console.log('after', store.getState())
  return result
}

Thunk 的核心是允许 dispatch 一个函数,由函数接收 dispatchgetState,完成请求后再派发普通 action。现代项目可使用 Redux Toolkit 简化 reducer、不可变更新和异步状态建模。

13. Immutable 与结构共享

不可变更新不是“任何嵌套对象都深拷贝”,而是只复制发生变化的路径,并复用未变化的引用:

const nextState = {
  ...state,
  user: {
    ...state.user,
    name: nextName,
  },
}

这样可以用引用比较快速判断哪些分支变化,也减少深拷贝成本。直接修改旧对象会让 PureComponentReact.memo 或 Redux selector 误判为未变化;使用 Immer/Redux Toolkit 时仍要遵守“一个 reducer 只描述一次明确更新”的原则。

14. SSR、静态资源与 hydration

React SSR 的基本链路是服务端生成 HTML,浏览器先展示内容,再由客户端 hydration 绑定事件和恢复交互:

请求 -> 服务端 renderToString -> HTML
     -> 浏览器绘制 -> 客户端 hydrate -> 可交互

SSR 的收益包括首屏内容和 SEO,代价包括服务端运行成本、数据预取复杂度和 hydration 不一致风险。服务端与客户端首次渲染必须得到一致的结构;依赖 window、随机数或当前时间的逻辑应延后到客户端 effect,或提供稳定初始值。

静态资源应由 Webpack/Vite 等构建工具处理并带有可缓存指纹,Express/Nginx 只负责正确提供构建产物和入口回退,不要在服务端渲染时把用户可控字符串直接拼进 HTML。

15. React 19 能力与服务端边界

React 19 的新增能力主要围绕异步 Action、表单状态、乐观更新、资源加载和服务端组件协作。面试时先说明使用的 React/框架版本,再谈 API,避免把框架层能力误说成 React 核心在所有环境都自动提供。

能力 解决的问题 需要补充的边界
useActionState / Actions 统一异步提交、pending 和错误结果 服务端写操作仍需鉴权、幂等和失败补偿
useFormStatus 在表单提交期间读取 pending 等状态 必须位于对应 <form> 的后代组件中
useOptimistic 在服务端结果返回前展示乐观状态 失败时要回滚,不能把乐观值当成已持久化事实
use 读取 Promise 或 Context 等资源 数据源、缓存和错误边界由框架/应用提供;不能绕过权限校验
ref 作为 prop 函数组件接收 ref 时减少部分包装代码 旧版本和部分库仍需 forwardRef,迁移要看编译目标
<Context> Provider 用更紧凑的 Provider 写法 只改变语法,不改变 Context 广播和重渲染成本
stylesheet/resource API 让渲染器声明资源的优先级和生命周期 CSP、缓存、预加载和 SSR 输出仍由部署环境负责

错误回调(如 onCaughtErroronUncaughtErroronRecoverableError)可以帮助把渲染错误、未捕获错误和可恢复问题分别上报,但不能替代事件处理器、网络请求和服务端日志的错误处理。useTransition 适合降低非紧急更新优先级,也不等于自动取消旧请求。

Server Components 与 SSR 不是同一个概念:SSR 负责生成 HTML,Server Components 决定某个组件是否只在服务端执行。任何从服务端边界传到客户端的 props 都要经过序列化和权限审查,数据库客户端、文件系统能力和密钥不能通过 props 暴露。需要交互的组件应尽量缩小 use client 边界,减少客户端 JavaScript 和数据泄露面。(来源:25年前端年度复盘.pdf 第 2 页)

16. 客户端状态库如何选型

状态库题不要只按 API 背诵,应先区分状态作用域、更新频率、异步需求和团队生态:

方案 适合的状态 主要取舍
Context 主题、语言、登录用户等低频跨层依赖 Provider value 变化会影响消费者,需拆分和稳定引用
Redux Toolkit 大型团队、可追踪事件流、复杂异步 约束和样板较多,但调试、审计和生态成熟
Zustand 小型到中型共享状态、按选择器订阅 API 简洁,团队需自行约定事件、持久化和调试规范
MobX 对象模型和细粒度响应式更新 隐式依赖较多,调试和跨团队约束要求更高
Jotai / Recoil 原子化状态和局部依赖 原子拆分灵活,要评估生态、并发和持久化方案
Valtio 代理驱动的局部状态 修改直观,但需要明确序列化、调试和不可变边界

服务端缓存和客户端 UI 状态不要混在一个 store 中。查询缓存应有 key、过期、失效和并发去重策略;表单输入、弹窗和选中项则通常属于页面或组件局部状态。无论选哪种库,都要能解释订阅粒度、异步错误、SSR 隔离和测试方式。

16.1 SWR:先展示可用数据,再后台重新验证

stale-while-revalidate(SWR)适合允许短暂陈旧的读请求:先按稳定的查询 key 返回缓存,页面立即展示可用数据;缓存过期或主动失效后,在后台发起新请求,成功时替换缓存并通知订阅者,失败时保留旧数据并暴露错误和重试入口。它不是“永远优先用缓存”,也不能用于需要强一致或有副作用的写操作。

实现或选库时至少明确:

  • key 是否包含租户、用户、筛选条件和 API 版本,避免跨权限复用缓存;
  • freshstaleerror 的展示语义,TTL、主动失效和窗口聚焦重验证策略;
  • 同一 key 的请求去重、旧响应覆盖新响应的竞态处理,以及组件卸载后的取消;
  • SSR 首次数据如何注水到客户端、缓存如何脱敏,以及登出或权限变化时如何清理。

面试中可以用“缓存命中 -> 后台 revalidate -> 原子替换 -> 失败保留旧值”的状态图说明机制,再用命中率、请求延迟、错误率和数据新鲜度验证是否真的改善首屏体验。该思路对应第一篇资料中的首页接口缓存案例,具体 TTL 和收益必须以项目实测为准。

面试口述模板

React 题目可以按“组件模型 -> 状态流 -> 协调机制 -> 工程取舍”回答:先说明 UI 是状态的函数,再说明更新如何通过 Fiber 和 Diff 进入提交阶段,最后结合受控表单、性能、路由或 SSR 场景说明为什么选择某种实现。遇到旧版类组件题目时,补充对应的 Hooks 写法和迁移边界即可。