📅 Day 3:Hooks 原理
学习目标:彻底搞懂 React 是怎么记住你的 state 的
👴 老大爷能听懂版
Hooks 是啥?
想象你在流水线工厂上班:
| 流水线上的东西 | 对应 Hooks |
|---|---|
| 第一个工位的冰箱 | useState(存状态) |
| 第二个工位的定时器 | useEffect(定时干活) |
| 第三个工位的对讲机 | useContext(跨部门喊话) |
| 每个工位有个编号 | Hooks 调用顺序(不能乱!) |
简单说:Hooks 就是给函数组件提供"状态"和"生命周期"的工具。
React 怎么记住你的 state?
想象一个火车站的寄存柜:
javascript
// 你写的代码
function App() {
const [count, setCount] = useState(0);
// ...
}
React 背后干的活:
yaml
┌─────────────────────────────────────────────┐
│ 组件 (App) 的"行李" │
├─────────────────────────────────────────────┤
│ Hook 1: { state: 0, next: Hook2 } │ ← useState(0)
│ Hook 2: { effect: ..., next: null } │ ← useEffect(...)
│ Hook 3: { context: ..., next: null } │ ← useContext(...)
└─────────────────────────────────────────────┘
↓
链表串起来!
关键点:Hooks 是用"链表"存储的!
为什么 Hooks 必须按顺序调用?
想象点菜:
javascript
// ✅ 正确:每次都按顺序点
function App() {
const [a, setA] = useState(1); // 第一个菜:宫保鸡丁
const [b, setB] = useState(2); // 第二个菜:麻婆豆腐
const [c, setC] = useState(3); // 第三个菜:鱼香肉丝
}
// ❌ 错误:顺序乱了
function App() {
const [a, setA] = useState(1);
if (show) {
const [b, setB] = useState(2); // 这菜不一定点!
}
const [c, setC] = useState(3); // 顺序全乱了!
}
原因:React 用"序号"找 Hook,就像餐厅按顺序上菜,顺序乱了就读错数据了!
useState 怎么工作的?
场景:买奶茶
| 步骤 | 实际发生的事 |
|---|---|
| init:你要一杯奶茶 | useState(0) 创建初始 state |
| 存:服务员记录下来 | React 把它存到 Hook 对象的 memoizedState |
| 改:你说"换成半糖" | 调用 setCount(1),触发重新渲染 |
| 取:再要一杯 | React 再次运行组件,从 Hook 里取出 memoizedState |
useEffect 怎么工作的?
场景:设闹钟
javascript
useEffect(() => {
console.log('组件挂载了!');
return () => console.log('清理!');
}, [依赖]);
| 阶段 | 比喻 | 实际发生 |
|---|---|---|
| 组件挂载 | 闹钟响了 | 执行回调函数 |
| 依赖变化 | 重新设闹钟 | 先执行清理函数,再执行新回调 |
| 组件卸载 | 闹钟关了 | 执行清理函数 |
注意:useEffect 是在渲染完成后执行的,不是渲染期间!
useRef 和 useState 的区别?
场景:存钱罐 vs 记事本
| useState | useRef | |
|---|---|---|
| 比喻 | 存钱罐(变了会引起注意) | 记事本(变了不声张) |
| 修改后 | 触发重新渲染 | 不触发重新渲染 |
| 用途 | UI 需要跟着变的值 | 存 DOM 引用、计时器等 |
javascript
// useState:改了会重新渲染
const [count, setCount] = useState(0);
setCount(1); // 组件重新渲染!
// useRef:改了不重新���染
const countRef = useRef(0);
countRef.current = 1; // 不会重新渲染!
💻 专业开发者版
Hooks 的核心数据结构:链表
javascript
// packages/react-reconciler/src/ReactFiberHooks.js
// 每个 Hook 就是一个链��节点
const hook = {
memoizedState: null, // 存储的状态(useState 的值、useEffect 的回调等)
baseState: null, // 基础状态
baseQueue: null, // 基础队列
queue: null, // 更新队列
next: null, // 指向下一个 Hook
};
链表结构:
yaml
Fiber.memoizedState (第一个 Hook)
↓
Hook1 { state: 0, next: Hook2 }
↓
Hook2 { effect: fn, next: Hook3 }
↓
Hook3 { context: ..., next: null }
React 包中的 Hooks 入口
javascript
// packages/react/src/ReactHooks.js
// 所有 Hook 都长这样:找 dispatcher,然后把调用传给它
export function useState<S>(initialState) {
const dispatcher = resolveDispatcher(); // ← 找到实际的实现
return dispatcher.useState(initialState); // ← 调用它
}
function resolveDispatcher() {
// ReactSharedInternals.H 就是那个神秘的 H( dispatcher)
const dispatcher = ReactSharedInternals.H;
return dispatcher;
}
useState 源码解析
javascript
// packages/react-reconciler/src/ReactFiberHooks.js
// 1. 创建 Hook(链表节点)
function mountStateImpl(initialState) {
const hook = mountWorkInProgressHook(); // 创建新 Hook
// 存到 hook.memoizedState
hook.memoizedState = hook.baseState = initialState;
// 创建更新队列
const queue = {
pending: null, // 待处理的更新
lanes: NoLanes,
dispatch: null, // setXxx 函数
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
return hook;
}
// 2. 真正创建 useState
function mountState(initialState) {
const hook = mountStateImpl(initialState);
// 创建 dispatch 函数(就是 setXxx)
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
return [hook.memoizedState, dispatch]; // 返回 [值, set函数]
}
useEffect 源码解析
javascript
// useEffect 的数据结构
const fiberEffect = {
tag: HookLayout | HookPassive, // 标记类型
create: () => () => void, // 回调函数
destroy: () => void, // 清理函数
deps: Array, // 依赖数组
next: null, // 下一个 effect
};
执行时机:
- 渲染阶段:不做任何事情
- 渲染完成后(Commit 阶段):调度 effect
- 下一次绘制前:执行上次 cleanup + 新的 effect
面试必问题:为什么不能条件调用 Hooks?
javascript
// ❌ 错误示范
function App() {
const [a, setA] = useState(1);
if (condition) {
const [b, setB] = useState(2); // 可能不执行!
}
const [c, setC] = useState(3); // 这时候 c 可能错位到 b 的位置
}
// ✅ 正确示范
function App() {
const [a, setA] = useState(1);
const [b, setB] = useState(2); // 始终调用
const [c, setC] = useState(3); // 始终调用
// 用 if 控制业务逻辑,不要控制 Hooks 调用
if (condition) {
// 在这里用 b 的值
}
}
原因:Hooks 是按"调用顺序"存储在链表中的,第一次渲染时Hooks 是 [a, b, c],第二次如果 b 没调用,React 就会把 c 当成 b,导致数据错乱!
Hooks 调用规则源码验证
javascript
// packages/react/src/ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
// 如果 dispatcher 是 null,说明没在组件里调用
if (dispatcher === null) {
console.error(
'Invalid hook call. Hooks can only be called inside of ' +
'the body of a function component.'
);
}
return dispatcher;
}
📋 面试必考点
Q1:useState 和 useRef 的区别?
答:
useState useRef 修改后触发重渲染 ✅ 会 ❌ 不会 存储值的位置 memoizedStatememoizedState.current典型用途 UI 状态 DOM 引用、计时器、不触发渲染的值 一句话: useState 变了会让组件"重新出生",useRef 变了假装没看见。
Q2:为什么 Hooks 不能在条件/循环/嵌套函数中调用?
答:
- Hooks 依赖"调用顺序"来匹配对应的 state
- 顺序记录在链表里:
hook1.memoizedState→hook2.memoizedState- 如果顺序变了,React 就找不到对应的 state 了
- 这也是为什么 React 报错 "Rendered more hooks than during the previous render"
Q3:useEffect 的执行时机?
答:
- 渲染阶段:不执行
- 渲染完成后(Commit 阶段):调度 effect
- 下一个 paint 之前:执行上次 cleanup + 执行新 effect
- DOM 已经更新,所以可以安全操作 DOM
Q4:useLayoutEffect 和 useEffect 的区别?
答:
useEffect useLayoutEffect 执行时机 渲染完成后,paint 之后 渲染完成后,paint 之前 阻塞 DOM 更新? 不阻塞 阻塞 适用场景 数据获取、订阅 测量 DOM、修改 DOM 简单说: 需要"同步"操作 DOM 用 useLayoutEffect,其他用 useEffect。
Q5:useMemo 和 useCallback 的区别?
答: 本质都是"缓存"
useMemo(computeFn, deps)--- 缓存计算结果useCallback(fn, deps)--- 缓存函数本身 (等于useMemo(() => fn, deps))
📝 今日总结
| Hook | 老大爷版 | 程序员版 |
|---|---|---|
| useState | 冰箱(存食材) | 存状态,触发重渲染 |
| useEffect | 定时器闹钟 | 副作用,渲染后执行 |
| useRef | 记事本(不声张) | 存值不触发渲染 |
| useContext | 传菜窗口 | 跨组件共享数据 |
| useMemo | 备忘录 | 缓存计算结果 |
| useCallback | 记事本(函数版) | 缓存函数 |
🎯 今日自测
- Hooks 为什么必须按顺序调用?
- useState 返回的 setXxx 做了什么?
- useEffect 的清理函数什么时候执行?
- useRef 修改后为什么不会触发重渲染?
- useMemo 和 useCallback 的区别是?
📅 明日预告
Day 4:Fiber 架构
| 本日概念 | 明日进阶 |
|---|---|
| 链表存储 | Fiber 树结构 |
| 渲染后更新 | render vs commit 阶段 |
| Hook 顺序 | 异步调度 |
下节预告:React 是怎么"画"页面的?Fiber 架构大揭秘!🚀