React 知识点复习

React 进阶面试核心复习笔记:从 Hooks 底层到 RSC 架构

一、 Hooks 核心机制:为什么不能写在条件语句中?

💡 核心通关回答

React 底层使用单向链表(存储在 Fiber 节点的 memoizedState 上)按照固定的调用顺序来记录和更新 Hooks 的状态。React 在重新渲染时没有任何唯一标识(如 key)来匹配 Hook,全靠指针移动。如果将 Hook 写在条件语句中,导致前后渲染的 Hooks 数量或顺序错位,链表指针就会“串台”,引发极其严重的状态崩溃。

🛠️ 底层原理伪代码

let memorizedState = []; // 模拟 React 底层存储 hooks 的数组/链表
let cursor = 0;          // 游标

function useState(initialValue) {
  if (memorizedState[cursor] === undefined) {
    memorizedState[cursor] = initialValue;
  }
  const currentCursor = cursor;
  const state = memorizedState[currentCursor];
  const setState = (newValue) => {
    memorizedState[currentCursor] = newValue;
    triggerRender(); 
  };
  cursor++; // 💥 强依赖调用顺序:每次执行完游标+1
  return [state, setState];
}

二、 useEffect vs useLayoutEffect

💡 核心通关回答

两者的根本区别在于执行时机

  • useLayoutEffect:在 DOM 结构更新后、浏览器把像素点画到屏幕上(Paint)之前同步执行。会阻塞浏览器渲染。
  • useEffect:在 浏览器把像素点画到屏幕上(Paint)之后异步执行。不会阻塞浏览器渲染。
[DOM 改变] -> [执行 useLayoutEffect (阻塞)] -> [浏览器绘制 Paint] -> [执行 useEffect (异步)]

🛠️ 实战避坑场景

  • 必须使用 useLayoutEffect 的场景:需要通过 JS 计算 DOM 几何属性(如 getBoundingClientRect)并立刻修改样式或位置。如果此时用 useEffect,用户会看到明显的肉眼可见的“闪烁”。

三、 Fiber 架构与双缓存机制

💡 核心通关回答

  • 解决的痛点:React 15 时代采用递归比对虚拟 DOM(Stack Reconciler),一旦开始无法中断,当 DOM 树过深时会长时间霸占主线程,导致页面卡死。
  • Fiber 的救场方案:Fiber 本质上是一个执行单元,也是一种链表数据结构(通过 childsiblingreturn 指针连接)。它把“死板的递归”改成了“可中断的循环”,让 React 可以边断边续,把控制权随时还给浏览器。
  • 双缓存机制:内存中同时存在 current 树(对应当前屏幕真实 DOM)和 workInProgress 树(正在构建的新状态)。计算完成后,只需改变根节点指针,即可瞬间完成页面更新,有效防止画面撕裂。
[current 树] <--- 对应屏幕 DOM
      |  (指针切换)
[workInProgress 树] <--- 内存中默默计算

四、 React 18 并发机制与时间分片

💡 核心通关回答

JS 是单线程的,React 的“并发”是指同时处理多个状态更新的能力

  • 时间分片:依靠内置的 Scheduler(调度器),React 限制每次 JS 连续执行时间不超过 5ms
  • 插队机制:如果 5ms 时间到但任务没执行完,React 会通过 MessageChannel 宏任务主动让出主线程,优先让浏览器响应高优先级的用户输入或动画,等浏览器空闲后再“断点续传”低优先级任务。

五、 状态管理:Context 性能痛点与 Zustand 优势

💡 核心通关回答

  1. React Context 痛点穿透更新带来的全量重渲染。只要 Context 的 value 发生变化,所有消费该 Context 的组件都会被迫重新渲染,无法做到精确的“按需更新”。
  2. Zustand 破局方案
  • 去上下文依赖:Store 本质是一个脱离 React 的纯 JS 闭包对象。
  • 精准订阅:利用 Selector 机制(底层的 useSyncExternalStore),只有组件真正订阅的属性发生改变时,才会触发该组件的渲染。

🛠️ Context 极致优化范式(读写分离)

const UserInfoContext = createContext();
const UserActionContext = createContext(); // 存放长期不变的方法

export function AppProvider({ children }) {
  const [user, setUser] = useState({ name: 'Groot', age: 3 });
  
  // 保持 Action 引用不变,防止仅依赖方法的组件被误触发重渲染
  const actions = useMemo(() => ({
    updateName: (name) => setUser(prev => ({ ...prev, name }))
  }), []);

  return (
    <UserInfoContext.Provider value={user}>
      <UserActionContext.Provider value={actions}>
        {children}
      </UserActionContext.Provider>
    </UserInfoContext.Provider>
  );
}

六、 性能优化实战:无效重渲染治理

💡 核心通关回答

排查大页面卡顿的三步走策略:

  1. 定位:打开 React DevTools 的 Profiler 面板,勾选 “Record why each component rendered”,录制操作,揪出爆红的长耗时组件。
  2. 原因:通常是父组件状态频繁变动引发表层子组件跟着“无脑”刷新,或未正确处理引用导致 React.memo 失效。
  3. 治理
  • 状态下放:把局部状态(如 input、弹窗)抽离到独立小组件中。
  • 内容留白:利用将大组件作为 children 传入的方式,切断父组件状态关联。

七、 架构演进:SSR 与 RSC 的本质区别

💡 核心通关回答

维度SSR (服务端渲染)RSC (React 服务端组件)
运行阶段仅在首屏加载时运行于服务端。每次组件渲染时都可在服务端运行。
输出产物输出 HTML 字符串输出 特制的 JSON 格式虚拟 DOM 流
JS 体积无体积优化。客户端仍需下载全量 Bundle 完成注水(Hydration)。体积暴减。RSC 中的第三方库(如 500KB 的 Markdown 解析库)完全不会打入客户端 Bundle,体积为 0KB
组件状态刷新会导致页面重置。刷新服务端数据时,可完美保留客户端已有的输入框焦点、滚动位置等状态。