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 本质上是一个执行单元,也是一种链表数据结构(通过
child、sibling、return指针连接)。它把“死板的递归”改成了“可中断的循环”,让 React 可以边断边续,把控制权随时还给浏览器。 - 双缓存机制:内存中同时存在
current树(对应当前屏幕真实 DOM)和workInProgress树(正在构建的新状态)。计算完成后,只需改变根节点指针,即可瞬间完成页面更新,有效防止画面撕裂。
[current 树] <--- 对应屏幕 DOM
| (指针切换)
[workInProgress 树] <--- 内存中默默计算
四、 React 18 并发机制与时间分片
💡 核心通关回答
JS 是单线程的,React 的“并发”是指同时处理多个状态更新的能力。
- 时间分片:依靠内置的
Scheduler(调度器),React 限制每次 JS 连续执行时间不超过 5ms。 - 插队机制:如果 5ms 时间到但任务没执行完,React 会通过
MessageChannel宏任务主动让出主线程,优先让浏览器响应高优先级的用户输入或动画,等浏览器空闲后再“断点续传”低优先级任务。
五、 状态管理:Context 性能痛点与 Zustand 优势
💡 核心通关回答
- React Context 痛点:穿透更新带来的全量重渲染。只要 Context 的
value发生变化,所有消费该 Context 的组件都会被迫重新渲染,无法做到精确的“按需更新”。 - 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>
);
}
六、 性能优化实战:无效重渲染治理
💡 核心通关回答
排查大页面卡顿的三步走策略:
- 定位:打开 React DevTools 的 Profiler 面板,勾选 “Record why each component rendered”,录制操作,揪出爆红的长耗时组件。
- 原因:通常是父组件状态频繁变动引发表层子组件跟着“无脑”刷新,或未正确处理引用导致
React.memo失效。 - 治理:
- 状态下放:把局部状态(如 input、弹窗)抽离到独立小组件中。
- 内容留白:利用将大组件作为
children传入的方式,切断父组件状态关联。
七、 架构演进:SSR 与 RSC 的本质区别
💡 核心通关回答
| 维度 | SSR (服务端渲染) | RSC (React 服务端组件) |
|---|---|---|
| 运行阶段 | 仅在首屏加载时运行于服务端。 | 每次组件渲染时都可在服务端运行。 |
| 输出产物 | 输出 HTML 字符串。 | 输出 特制的 JSON 格式虚拟 DOM 流。 |
| JS 体积 | 无体积优化。客户端仍需下载全量 Bundle 完成注水(Hydration)。 | 体积暴减。RSC 中的第三方库(如 500KB 的 Markdown 解析库)完全不会打入客户端 Bundle,体积为 0KB。 |
| 组件状态 | 刷新会导致页面重置。 | 刷新服务端数据时,可完美保留客户端已有的输入框焦点、滚动位置等状态。 |