Skip to content

第 11 章 · React 入门 面试题集

Q1:JSX 是什么?它和 HTML 有什么区别?

考察点:JSX 本质、Babel 编译、虚拟 DOM 入口

标准答案: JSX 是 JavaScript 的语法扩展,允许在 JS 中书写类似 HTML 的标记。它不是 HTML,也不是字符串,而是会被 Babel(或 SWC)编译成 React.createElement(type, props, ...children) 调用,最终生成普通 JS 对象(虚拟 DOM 节点)。

通俗理解: JSX 像"中英混合"的文档——你在 JS 里夹了几段 HTML 风格的描述,但发出去之前要被翻译成纯 JS 才能给浏览器看。

代码示例

jsx
const el = <h1 className="t">Hi {name}</h1>;
const el = React.createElement('h1', { className: 't' }, 'Hi ', name);

加分回答

  • JSX 与 HTML 的差异:className 替代 classhtmlFor 替代 for,事件用驼峰,style 是对象,标签必须自闭合,必须有唯一根节点
  • React 17 后引入了新的 JSX Transform,无需手动 import React
  • JSX 中只能写表达式不能写语句,所以条件渲染用三元/&&,循环用 map

追问

  • 为什么 JSX 中 falsenullundefined 不会被渲染?
  • <input> 在 React 里需要写成 <input /> 吗?

Q2:useState 的工作原理是什么?为什么必须在组件顶层调用?

考察点:Hooks 规则、闭包、Fiber 链表存储

标准答案: React 内部用一条链表保存每个组件实例的所有 Hook 状态。每次组件渲染时,按调用顺序依次取出对应位置的 state。如果在条件、循环或嵌套函数中调用 Hooks,会破坏调用顺序,导致 state 错位。

通俗理解: React 不靠"名字"找你的状态,而是靠"第几个调用"。就像点菜按桌号排队,如果你今天是第 1 桌、明天又突然插队变成第 2 桌,菜就上错了。

代码示例

jsx
function Comp() {
  const [a, setA] = useState(0);  // hook[0]
  const [b, setB] = useState(0);  // hook[1]
  // ❌ 错误:条件里的 Hook
  if (cond) {
    const [c, setC] = useState(0); // 有时是 hook[2],有时不是!
  }
}

加分回答

  • Hooks 规则由 ESLint 插件 eslint-plugin-react-hooks 强制
  • React Fiber 节点的 memoizedState 字段就是这条 Hook 链表
  • 函数组件每次渲染都重新执行,闭包陷阱(Stale Closure)由此产生

追问

  • 为什么 setState(prev => prev + 1)setState(state + 1) 安全?
  • React 18 的自动批处理对 setState 有什么影响?

Q3:useEffect 的依赖数组怎么用?三种情况分别是什么?

考察点:副作用、闭包、依赖完整性

标准答案

形式行为
useEffect(fn)每次渲染后都执行
useEffect(fn, [])仅挂载/卸载时执行(首次执行 fn,卸载执行清理)
useEffect(fn, [a, b])ab 通过 Object.is 比较发生变化时执行

返回的清理函数会在下次副作用执行前组件卸载时调用。

通俗理解: 依赖数组就是"什么时候需要重新跑这段副作用"的清单。空数组 = 只跑一次;不写 = 永远跟随渲染;写了 = 这些值变了就重跑。

代码示例

jsx
useEffect(() => {
  const id = setInterval(() => setN(n => n + 1), 1000);
  return () => clearInterval(id);
}, []);

加分回答

  • 漏写依赖会引发"陈旧闭包"(Stale Closure):副作用里读到旧的 props/state
  • ESLint 规则 react-hooks/exhaustive-deps 自动校验
  • 想在依赖变化时跳过首次执行,可以用 useRef 标记是否首次

追问

  • useEffect 里 setState 会不会死循环?怎么避免?
  • React 严格模式下 useEffect 为何会被调用两次?

Q4:函数组件 vs 类组件,为什么 React 推荐函数组件?

考察点:架构演进、Hooks 优势

标准答案

  • 没有 this 困扰:函数组件每次渲染都是纯函数调用,无 bind、无 this 指向问题
  • 逻辑可复用:通过自定义 Hooks 抽离状态逻辑,比类组件的 HOC、Render Props 更优雅
  • 更小的代码体积:函数组件无需类继承的运行时开销
  • 更适合并发模式:Fiber + 函数组件让暂停/恢复渲染更安全
  • 状态逻辑可组合useState + useEffect 比类组件分散在各生命周期更聚合

通俗理解: 类组件像"重型相机"——功能全但操作复杂,按钮一大堆。函数组件像"手机相机"——直觉好,按一下就拍,还能装各种 App(Hooks)扩展。

代码示例

jsx
class Old extends React.Component {
  state = { n: 0 };
  add = () => this.setState({ n: this.state.n + 1 });
  render() { return <button onClick={this.add}>{this.state.n}</button>; }
}

function New() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n+1)}>{n}</button>;
}

加分回答

  • React 18 之后并发特性(useTransitionuseDeferredValue)只能在函数组件中使用
  • 类组件仍然支持,但不会再增加新特性

追问

  • 为什么 Hooks 不能在类组件中使用?
  • 函数组件每次渲染都重建函数,会不会有性能问题?

Q5:受控组件和非受控组件的区别?

考察点:表单处理、单向数据流

标准答案

  • 受控组件:表单元素的值由 React state 控制,每次输入都触发 onChange 更新 state
  • 非受控组件:表单元素的值由 DOM 自己管理,通过 ref 在需要时读取
jsx
// 受控
<input value={name} onChange={e => setName(e.target.value)} />

// 非受控
const ref = useRef();
<input defaultValue="初始" ref={ref} />
<button onClick={() => console.log(ref.current.value)}>提交</button>

通俗理解: 受控 = 你时时刻刻盯着表单,每个字符都告诉你;非受控 = 你不管,提交时再去拿结果,就像传统 HTML form。

加分回答

  • 受控组件便于实时校验、联动
  • 非受控适合"一次性提交"的场景或集成第三方 DOM 库
  • <input type="file"> 必须用非受控(受 React 限制)

追问

  • 大表单(100 个字段)用受控会有性能问题吗?怎么优化?
  • defaultValuevalue 的区别?

Q6:React 的事件机制是什么?为什么叫合成事件?

考察点:事件委托、跨浏览器一致性

标准答案: React 不是把事件直接绑定在 DOM 节点上,而是统一委托到根容器(React 17 之前是 document,17+ 是 root 容器)。当事件冒泡到根节点时,React 用合成事件(SyntheticEvent)对原生事件做了一层包装,提供跨浏览器一致的 API。

通俗理解: 传统是"每个员工自己接电话";React 是"前台统一接,再分发给对应员工"——节省内存、统一处理。

代码示例

jsx
<div onClick={parent}>
  <button onClick={child}>click</button>
</div>
// child 先执行,再冒泡到 parent

加分回答

  • 合成事件对象会被复用(事件池),React 17 后取消事件池
  • React 17 改成挂载到 root 容器,便于多版本 React 共存
  • 阻止冒泡:e.stopPropagation();阻止默认行为:e.preventDefault()
  • 想监听原生事件用 e.nativeEvent

追问

  • React 合成事件能阻止原生事件冒泡吗?
  • 为什么 React 17 之前不能在原生事件里阻止 React 事件?

Q7:useState 和 useRef 有什么区别?

考察点:响应式 vs 引用

标准答案

维度useStateuseRef
修改是否触发重渲染✅ 是❌ 否
值的存储React 内部状态链表{ current: ... } 普通对象
用途影响 UI 的数据DOM 引用、定时器 id、上一次的值
跨渲染保持

通俗理解: useState 是"公告板"——一改全公司都看到(重新渲染);useRef 是"私人小本本"——你随便记,没人会因为你改了它而做出反应。

代码示例

jsx
const inputRef = useRef(null);
useEffect(() => inputRef.current.focus(), []);
return <input ref={inputRef} />;

加分回答

  • useRef 还能保存"前一次的 props/state"用于对比
  • React 18 的 useSyncExternalStore 配合 ref 可订阅外部 store

追问

  • 父组件能拿到子函数组件的 ref 吗?怎么做?(forwardRef + useImperativeHandle)
  • ref 在初次渲染时一定能拿到 DOM 吗?

Q8:key 在列表渲染中起什么作用?为什么不推荐用 index 作为 key?

考察点:Diff 算法、虚拟 DOM 复用

标准答案: React 通过 key 在 Diff 时识别"哪个元素是同一个",决定复用、移动、新增、删除key 必须在兄弟节点中唯一且稳定。用 index 当 key,在列表顺序变化(增删、排序)时,React 会误以为元素被"修改"而非"移动",导致:

  • 内部状态错位(如 input 文字串到错误项)
  • 不必要的 DOM 操作,性能下降

通俗理解key 像学生学号——按学号点名才能精准找到本人;如果按"座位号"点名(index),换座位后就乱套了。

代码示例

jsx
{list.map(item => <Row key={item.id} data={item} />)} // ✅
{list.map((item, i) => <Row key={i} data={item} />)} // ⚠️ 仅静态列表可用

加分回答

  • React Diff 是"同层比较",跨层级移动会被销毁重建
  • key 不要用 Math.random(),每次都不同 = 每次都重建
  • 列表纯展示且永不变 → 用 index 也可

追问

  • 假设列表有 1000 项,删除第一项,用 index 当 key 会发生什么?
  • React 的 Diff 复杂度是多少?怎么做到 O(n) 的?

Q9:在 React 中如何发起 HTTP 请求?

考察点:useEffect 异步、清理、竞态

标准答案: 在 useEffect 中发起请求,依赖数组放入触发请求的变量;返回清理函数取消请求或忽略过期响应,避免竞态

代码示例

jsx
function User({ id }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let ignore = false;
    fetch(`/api/user/${id}`)
      .then(r => r.json())
      .then(data => { if (!ignore) setUser(data); });
    return () => { ignore = true; };
  }, [id]);

  if (!user) return <p>Loading...</p>;
  return <div>{user.name}</div>;
}

通俗理解: 你点了 A,又快速点了 B。两个请求都在飞,万一 A 比 B 慢回来,画面会先 B 后 A——明明你看的是 B 却显示 A。ignore 就是"过期作废"的标记。

加分回答

  • 真实项目用 React Query / SWR,自动处理缓存、重试、竞态、轮询
  • AbortController 可以真正取消 fetch,而不是仅忽略结果
  • React 18 + Suspense for Data 是未来方向

追问

  • 为什么不在组件函数体里直接 fetch
  • Strict Mode 下 useEffect 调两次会不会发两次请求?怎么处理?

Q10:React 的虚拟 DOM 是什么?为什么需要它?

考察点:虚拟 DOM 概念、Diff、性能权衡

标准答案: 虚拟 DOM 是用 JS 对象描述真实 DOM 结构的轻量数据结构。每次状态变化,React 先生成新的虚拟 DOM 树,与上一次对比(Diff),算出最小的"补丁",再批量更新真实 DOM。

好处

  1. 抹平差异:声明式 UI,开发者无需手动 DOM 操作
  2. 跨平台:同一套 React 可输出 Web(react-dom)、原生(react-native)、SVG
  3. 批量优化:合并多次更新,减少 reflow/repaint

通俗理解: 直接改装修房子(操作 DOM)很贵。虚拟 DOM 像在 CAD 软件里画图——先在电脑里把改动画出来,对比哪里要动,确定方案后再让工人一次性施工。

代码示例

js
// 虚拟 DOM 节点
{
  type: 'div',
  props: {
    className: 'box',
    children: [
      { type: 'h1', props: { children: 'Hello' } }
    ]
  }
}

加分回答

  • 虚拟 DOM 不一定比直接操作 DOM 快——它的价值在于"上限够高、下限可控、心智负担低"
  • React 的 Diff 算法是 O(n),靠三个启发式:同层比较、type 不同直接换、列表用 key
  • Svelte / Solid 没有虚拟 DOM,编译时直接生成精确更新代码,运行时更快但灵活性较低

追问

  • 虚拟 DOM 一定比原生 DOM 操作快吗?
  • React Fiber 与"老的 Reconciler"有什么区别?(下一章会展开)

Q11(追加):React 18 有哪些重要更新?

考察点:版本演进、并发渲染

标准答案

  • 自动批处理:Promise、setTimeout、原生事件中的多次 setState 也会被批处理
  • 并发特性useTransitionuseDeferredValue,区分紧急 vs 非紧急更新
  • Suspense for Data:流式 SSR + 服务端组件
  • 新的 root APIcreateRoot(container).render(<App />)
  • 严格模式:开发阶段额外执行一遍组件/effect,暴露副作用问题

通俗理解: React 18 = "并发渲染"。以前像单核 CPU,一次只能跑完一件事;现在可以"中断 + 优先级排序",让用户输入这种紧急任务先响应,长列表渲染往后排。

追问

  • useTransition 能解决什么场景的卡顿?
  • React Server Components 跟 SSR 有什么区别?