主题
第 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替代class,htmlFor替代for,事件用驼峰,style是对象,标签必须自闭合,必须有唯一根节点 - React 17 后引入了新的 JSX Transform,无需手动
import React - JSX 中只能写表达式不能写语句,所以条件渲染用三元/
&&,循环用map
追问:
- 为什么 JSX 中
false、null、undefined不会被渲染? <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]) | 当 a 或 b 通过 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 之后并发特性(
useTransition、useDeferredValue)只能在函数组件中使用 - 类组件仍然支持,但不会再增加新特性
追问:
- 为什么 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 个字段)用受控会有性能问题吗?怎么优化?
defaultValue和value的区别?
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 引用
标准答案:
| 维度 | useState | useRef |
|---|---|---|
| 修改是否触发重渲染 | ✅ 是 | ❌ 否 |
| 值的存储 | 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。
好处:
- 抹平差异:声明式 UI,开发者无需手动 DOM 操作
- 跨平台:同一套 React 可输出 Web(react-dom)、原生(react-native)、SVG
- 批量优化:合并多次更新,减少 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 也会被批处理
- 并发特性:
useTransition、useDeferredValue,区分紧急 vs 非紧急更新 - Suspense for Data:流式 SSR + 服务端组件
- 新的 root API:
createRoot(container).render(<App />) - 严格模式:开发阶段额外执行一遍组件/effect,暴露副作用问题
通俗理解: React 18 = "并发渲染"。以前像单核 CPU,一次只能跑完一件事;现在可以"中断 + 优先级排序",让用户输入这种紧急任务先响应,长列表渲染往后排。
追问:
useTransition能解决什么场景的卡顿?- React Server Components 跟 SSR 有什么区别?