🔥 React 高频面试题:原理剖析(实战篇)

197 阅读8分钟

🔥 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.jsproduce(state, draft => {})
跨组件状态共享Context + useReducer创建全局状态上下文
表单控件受控组件 + useState<input value={val} onChange={e => setVal(e.target.value)}>

Q2

面试官 👨💻:useEffect 递归陷阱如何解决

候选人 🚀:

递归陷阱的产生

假设你在 useEffect 内部无条件地更新了某个 state,而且这个 state 又被加入了依赖数组中,这会导致以下流程:

  1. 组件渲染:初始渲染后,useEffect 会执行。
  2. 更新状态:在 useEffect 内调用 setState 更新了依赖中的 state。
  3. 重新渲染:因为 state 更新了,组件重新渲染。
  4. 依赖变化:新的渲染中,依赖数组检测到 state 与上一次不同,导致 useEffect 再次执行。
  5. 无限循环:如此不断重复,就形成了“递归陷阱”或无限更新循环。
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 !== NaN
    • Object.is(NaN, NaN) 则返回 true
  • 正负零的区分
    • === 认为 +0 === -0
    • Object.is(+0, -0) 返回 false

state 更新流程

  1. 当你调用 setState(如 setCount(count + 1))时:

    • React 会将新的 state 与旧的 state 进行比较(通常是引用比较或基本类型的值比较)。
    • 如果不同,触发一次新的渲染。
  2. 在新的渲染中,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 为复杂状态逻辑设计的「原子核反应堆」,当你的状态管理出现以下特征时,它就派上用场了:

  1. 状态间存在强关联(如表单验证、多步骤流程)
  2. 需要基于前状态计算新状态
  3. 涉及深层嵌套对象/数组的更新
  4. 需要可预测的状态变更轨迹(如实现时间旅行调试)

一、核爆现场: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 的最佳时机!

持续更新中... 🚀