一句话总结
React 的本质是把 UI 表达为状态的函数:UI = f(state)。函数组件每次渲染都是拿着当时快照的纯执行;Hooks 借助"调用顺序"这一隐式约定挂载状态;性能优化的核心永远是减少不必要的渲染与调和。
| 主题 | 高频考点 |
|---|---|
| Hooks | 闭包陷阱、依赖数组、useEffect 清理 |
| 渲染 | Fiber、diff、key 的作用 |
| 状态 | Redux/Zustand/Context 选型 |
| 性能 | memo/useMemo/useCallback/虚拟列表 |
Hooks 原理与闭包陷阱
函数组件没有"实例",React 把每个组件的状态按调用顺序串成链表挂载在 Fiber 上。这就是为什么 Hooks 不能写在条件/循环里——顺序乱了,状态就对不上了。
闭包陷阱:如下代码点击后拿到的总是初始 count,因为回调函数捕获了首次渲染的快照:
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 永远拿到首次渲染的 count
}, 1000);
return () => clearInterval(id);
}, []); // 依赖空,effect 只跑一次
修正:用函数式更新 setCount(c => c + 1),或把 count 列入依赖。理解"每次渲染都是独立闭包"是避坑关键。
渲染机制与 Fiber
调和(Reconciliation):状态变化触发重新渲染,React 用虚拟 DOM diff 找出最小变更再更新真实 DOM。key 帮助 diff 识别列表元素身份,缺失或不稳会导致整列重建。
- Fiber 架构:把渲染拆成可中断、可恢复的小工作单元,避免长任务阻塞主线程,支撑并发特性(Suspense、transition)。
- 合成事件:React 把事件委托到根节点,批量处理并做跨浏览器兼容。
- 批处理:React 18 起所有更新(含 Promise/timeout 内)默认自动批处理,减少冗余渲染。
状态管理选型
没有银弹,按作用域与更新频率选:
- useState/useReducer + Context:局部或低频全局状态;注意 Context 值变化会使所有消费者重渲染,别放高频状态。
- Zustand:轻量、按需订阅、无 Provider 嵌套,适合中大型应用,API 极简。
- Redux Toolkit:强约束、可调试(时间旅行)、适合超大型团队协作与复杂副作用;样板代码多。
- Server State:用 React Query/SWR 管理,别和客户端状态混为一谈。
性能优化
优化前先用 React DevTools Profiler 定位真正的热点,避免盲目 memo:
React.memo:跳过 props 未变的子组件重渲染(浅比较)。useMemo / useCallback:缓存昂贵计算与稳定函数引用,避免子组件无谓更新。- 列表虚拟化:万级列表用 react-window 只渲染可视区,DOM 数量恒定。
- 代码分割:
React.lazy + Suspense按需加载路由,减小首包。
常见误区:给所有组件包 memo、把每个函数都 useCallback——反而增加比对开销,应针对实测瓶颈。
面试模拟
Q1:为什么 Hooks 不能写在 if/for 里?
答:状态按调用顺序挂载,条件分支会导致顺序错乱,React 无法把状态对应到正确的 Hook。
Q2:useEffect 依赖为空数组代表什么?
答:effect 仅在挂载时执行一次、卸载时清理;若内部用到外部变量却未列依赖,会捕获旧快照(闭包陷阱)。
Q3:key 为什么不能用 index?
答:index 在数据增删/排序时会错位,导致组件状态错配;应用稳定唯一 id。
Q4:React.memo 一定能提速吗?
答:不一定。memo 本身有浅比较成本,且当 props 每次都变化时 memo 无效;应先 Profiler 定位真正的高频重渲染。