🔥 React 高频面试题:原理剖析(实战篇)
Q1
面试官 👨💻:useState传递函数有什么作用?
候选人 🚀:useState 其实可以传入值,也可以传入函数。当传入函数时,它只会在初始化时调用一次,用于惰性初始化(Lazy Initialization)
重点
- 同步更新状态,不适合api请求
- 只有在组件首次渲染时,
useState才会执行传入的初始化函数,而不会在每次组件重新渲染时都执行。这样可以提高性能,避免不必要的计算。
const [count, setCount] = useState(() => {
console.log("计算初始值");
return Math.random() * 100
})
源码分析
function mountState(initialState) {
// 判断 initialState 是否是一个函数
const state = typeof initialState === 'function' ? initialState() : initialState;
const hook = mountWorkInProgressHook();
hook.memoizedState = state;
return [state, dispatch];
}
- 如果
initialState是函数,则执行它并返回计算结果(惰性初始化)。 - 如果是普通值,直接返回。
Q1.2
面试官 👨💻:你在代码中看到setState(prev => prev + 1)这种写法,为什么这里要传函数而不是直接传值?
候选人 🚀:
前言:破解useState函数式更新的迷雾——你以为的“最新值”真的是最新的吗?
面试官视角:
"当我问出这个问题时,80%的候选人只能答出『避免状态合并』这种表面答案,却不知道这背后藏着React最核心的闭包陷阱和批量更新机制..."
开发者血泪史:
你有没有遇到过这些灵异现象?
- 🕳 点击按钮多次,状态却只更新了一次
- 👻 在定时器里拿到的是“过期”的状态值
- 🌀 复杂交互中状态出现不可预测的跳变
这不是一个简单的语法问题,而是React状态管理的核心机制!
我们将通过一个看似简单的setCount(prev => prev + 1),揭开:
1️⃣ 闭包的时间胶囊效应:为什么你的状态会“穿越”到过去?
2️⃣ 批量更新的幕后交易:React如何背着我们合并状态更新?
3️⃣ 函数式更新的量子跃迁:如何确保状态跨越渲染周期正确传递?
// 经典闭包陷阱案例
const [count, setCount] = useState(0);
// ❌ 危险操作:连续点击只会+1
const brokenIncrement = () => {
setTimeout(() => {
setCount(count + 1); // 永远在闭包里捕获初始值
}, 1000);
};
// ✅ 正确解法:量子隧穿式更新
const quantumIncrement = () => {
setTimeout(() => {
setCount(prev => prev + 1); // 穿透时间屏障获取最新值
}, 1000);
};
// 源码视角
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
- 如果
action是函数,则执行action(state)。 - 如果
action不是函数,则直接赋值。
总结
✅ 惰性初始化:
useState(() => initialValue)只有初始化时才会执行,提高性能,避免不必要计算。- 适用于 计算复杂的初始状态 或 API 请求。
✅ setState 传递函数的作用:
setState(prev => prev + 1)可以避免状态更新丢失。- 适用于多个
setState操作一起执行的情况(批量更新)。 - 在异步环境下更加安全,保证
state计算正确。
🔍 深度源码解析
useState内部会检查传入的初始值是否是函数,如果是函数,则调用它并存储结果。setState处理更新时,也会检查是否传入了一个函数,如果是,则用当前state作为参数执行它。
👉 结论
useState传入函数是 惰性初始化,只在初始化时执行,提升性能。setState也可以接收函数,可以安全地更新状态,避免丢失state。
性能优化表
| 场景 | 推荐方案 | 代码示例 |
|---|---|---|
| 复杂计算初始值 | 惰性初始化 | useState(() => heavyCalc()) |
| 高频更新 | useReducer | 见下文 |
| 深层对象更新 | Immer.js | produce(state, draft => {}) |
| 跨组件状态共享 | Context + useReducer | 创建全局状态上下文 |
| 表单控件 | 受控组件 + useState | <input value={val} onChange={e => setVal(e.target.value)}> |
Q2
面试官 👨💻:useEffect 递归陷阱如何解决
候选人 🚀:
递归陷阱的产生
假设你在 useEffect 内部无条件地更新了某个 state,而且这个 state 又被加入了依赖数组中,这会导致以下流程:
- 组件渲染:初始渲染后,useEffect 会执行。
- 更新状态:在 useEffect 内调用 setState 更新了依赖中的 state。
- 重新渲染:因为 state 更新了,组件重新渲染。
- 依赖变化:新的渲染中,依赖数组检测到 state 与上一次不同,导致 useEffect 再次执行。
- 无限循环:如此不断重复,就形成了“递归陷阱”或无限更新循环。
const [count, setCount] = useState(0);
useEffect(() => {
// 无条件更新 count
// 每次 useEffect 运行都会将 count 增加 1,从而导致依赖(count)始终在更新,形成无限循环。
setCount(count + 1);
}, [count]);
// 源码视角
// 每次渲染后,会保存当前依赖的值:prevDeps = currentDeps
function shouldRunEffect(prevDeps, currentDeps) {
// 对于每个依赖,React 使用 Object.is 进行比较
for (let i = 0; i < currentDeps.length; i++) {
if (!Object.is(prevDeps[i], currentDeps[i])) {
return true;
}
}
return false;
}
小知识
- 在大多数情况下,
Object.is和===返回的结果是一致的,但两者有一些细微的区别,使得Object.is在某些场景下更直观、更准确: - NaN 比较
===会认为NaN !== NaNObject.is(NaN, NaN)则返回true
- 正负零的区分
===认为+0 === -0Object.is(+0, -0)返回false
state 更新流程
-
当你调用 setState(如 setCount(count + 1))时:
- React 会将新的 state 与旧的 state 进行比较(通常是引用比较或基本类型的值比较)。
- 如果不同,触发一次新的渲染。
-
在新的渲染中,useEffect 会接收到更新后的依赖值:
- 如果检测到依赖变化(例如 count 从 0 变为 1),则会安排 effect 执行。
- 因为 effect 中又执行了 setState,导致新的更新,这个流程不断循环。
3. 如何避免递归陷阱
(1)条件判断
在 effect 中更新 state 时,添加条件判断来确保只有在满足某个条件时才更新 state。例如:
复制编辑
useEffect(() => {
// 只有当 count 小于 10 时,才进行更新
if (count < 10) {
setCount(count + 1);
}
}, [count]);
这样,一旦 count 达到 10,就不会再更新,从而避免无限循环。
(2)重新思考依赖项
- 有时你可能并不需要将某个值加入依赖数组。如果你确定这个值不会改变或者不影响 effect 的执行,可以将其移除(但要小心可能引起 stale closure 问题)。
- 使用 useRef 来存储那些不需要触发重新渲染的变量。useRef 的更新不会导致组件重新渲染,从而打断循环。
(3)分离副作用
- 将会导致状态更新的副作用与其他逻辑分离,避免在 effect 中直接进行会引发循环的状态更新。
- 如果更新逻辑比较复杂,可以考虑将其封装成自定义 Hook,并通过条件判断控制更新。
Q3
面试官 👨💻:为什么要用 useReducer 而不是 useState 更新状态?
候选人 🚀:useReducer 是 React 为复杂状态逻辑设计的「原子核反应堆」,当你的状态管理出现以下特征时,它就派上用场了:
- 状态间存在强关联(如表单验证、多步骤流程)
- 需要基于前状态计算新状态
- 涉及深层嵌套对象/数组的更新
- 需要可预测的状态变更轨迹(如实现时间旅行调试)
一、核爆现场:useReducer 如何接管复杂状态?
1.1 典型案例:购物车状态管理
// 初始状态
const initialState = {
items: [],
total: 0,
isLoading: false,
error: null
};
// 状态处理器(Reducer)
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price
};
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload.id),
total: state.total - action.payload.price
};
case 'FETCH_START':
return { ...state, isLoading: true };
case 'FETCH_SUCCESS':
return { ...state, isLoading: false };
case 'FETCH_FAIL':
return { ...state, isLoading: false, error: action.payload };
default:
return state;
}
}
// 组件中使用
const [cartState, dispatch] = useReducer(cartReducer, initialState);
1.2 对比 useState 的混沌状态
// ❌ useState 的混乱管理
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const addItem = (item) => {
setItems(prev => [...prev, item]);
setTotal(prev => prev + item.price); // 需要手动同步多个状态
// 容易漏掉其他相关状态的更新
};
二、核反应原理:useReducer 源码解剖
2.1 核心源码结构(ReactFiberHooks.js)
function updateReducer(reducer, initialArg, init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 处理排队中的更新(形成环形链表)
let first = queue.first;
let newState = hook.memoizedState;
// 遍历更新队列
while (first !== null) {
const action = first.action;
// ⚛️ 关键步骤:调用 reducer 计算新状态
newState = reducer(newState, action);
first = first.next;
}
hook.memoizedState = newState;
return [newState, queue.dispatch];
}
A[dispatch(action)] --> B{更新入队}
B --> C[形成环形链表]
C --> D[React调度更新]
D --> E[进入渲染阶段]
E --> F[遍历更新队列]
F --> G[调用reducer计算新状态]
G --> H[更新memoizedState]
H --> I[触发组件渲染]
三、核裂变场景:何时必须使用 useReducer?
| 场景 | useState 痛点 | useReducer 解决方案 |
|---|---|---|
| 表单联动验证 | 多个状态需要同步更新,逻辑分散 | 统一处理所有关联状态变更 |
| 多步骤流程(如结账) | 步骤切换与数据提交逻辑耦合 | 通过 action 类型明确状态迁移 |
| 全局状态管理 | Context + useState 导致无关组件渲染 | Context + useReducer 优化性能 |
| 撤销/重做功能 | 需要记录状态历史,手动管理困难 | 天然支持状态快照和历史记录 |
| 复杂动画控制 | 多个动画参数需要同步更新 | 原子化更新所有动画参数 |
🚀 黄金法则:
- 当你的组件开始出现 超过3个关联的useState
- 当你的状态更新函数开始出现 "setX(x => ...)" 嵌套
- 当你发现 同一操作需要修改多个不相关状态
这就是切换到useReducer的最佳时机!
持续更新中... 🚀