第 15 章:常用 Hooks 逐个拆解
第 12 章说过,hook 就是挂在 fiber.memoizedState 链表上的节点。本章把剩下的常用 hook 逐个拆开,看它们在 memoizedState 里各存了什么、各有什么坑。
先记住这一句:这批 hook 没有秘密,全是一句话的机制:useRef 存一个永不换引用的对象,useMemo/useCallback 存
[值, deps]缓存,useContext 只是读栈,useId 存一个字符串,useSyncExternalStore 订阅外部数据源。
一图流:每个 hook 在 memoizedState 里存什么
useRef:一个永不变引用的对象
实现是最短的(mountRef):
function mountRef(initialValue) {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
function updateRef(initialValue) {
const hook = updateWorkInProgressHook();
return hook.memoizedState; // 原样返回同一个对象
}
updateRef(2610)每次渲染都返回同一个对象引用。所以两个经典行为都源于此:改 ref.current 不触发重渲染(没有 setState),以及 ref 对象在组件生命周期内身份稳定(可以当依赖数组里的"永远不变"项)。
useMemo 与 useCallback:一个 [值, deps] 缓存
两个函数几乎一样。useMemo(mountMemo / updateMemo):
// 更新时
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0]; // deps 没变,返回缓存的值
}
const nextValue = nextCreate(); // 变了,重新算
hook.memoizedState = [nextValue, nextDeps];
useCallback(updateCallback)逻辑一模一样,只是"缓存的值"就是回调函数本身。所以 useCallback 是 useMemo 的简化兄弟,记住一个就记住俩。
坑也在此:areHookInputsEqual 是引用比较(第 14 章那个 Object.is)。deps 数组里放了个每次新建的对象,就永远"变了",缓存失效。这就是"useMemo 依赖了不该依赖的引用导致白算"的根源。
useContext:只是 readContext
useContext 在 dispatcher 里的实现就一行(ReactFiberHooks.js:3929):useContext: readContext。真正的 readContext 在 ReactFiberNewContext.js:502,它做两件事:从 context 栈里取当前值,并把这个 context 登记进 fiber 的 dependencies。登记依赖的意义在第 10 章讲过:Provider 值变了,React 能顺着 dependencies 找到这个消费者,哪怕隔着 memo。
useId:一个稳定字符串
mountId(3450)按环境生成:
// 客户端
id = '_' + identifierPrefix + 'r_' + globalClientIdCounter++.toString(32) + '_';
// 服务端水合
id = '_' + identifierPrefix + 'R_' + treeId;
这就是 19 里 useId() 返回值长这样:_r_0_、_r_1_。19.2 从冒号改成了下划线(_:r0:_ → _r0_),原因就是冒号在 CSS 选择器里有特殊含义。它不是随机数,是计数器 + 树位置,保证同一棵树内唯一、水合时前后端对得上。
useSyncExternalStore:给外部数据源架桥
mountSyncExternalStore(1634)是 Zustand、Redux 这类库跟 React 对接的底层通道。核心是 getSnapshot() 拿值 + subscribe() 订阅变化,配合一个强制渲染检查:store 变了但快照没变就告警,防止死循环。它内部还会调用 useSyncExternalStore 自带的 useSyncExternalStore_shim 兼容旧版本。你要自己写一个订阅外部状态的 hook,它是最标准的模板。
动手实验
- 验证 ref 身份:组件里
const r = useRef(0); console.log(r);连续渲染几次,确认控制台打印的是同一个对象。对照updateRef的"原样返回"。 - 看 useMemo 缓存失效:
useMemo(() => 计算(), [obj])里obj每次都新建,在areHookInputsEqual打断点,看它每次返回 false、nextCreate每次都被调。 - 扒 useId:
useId()打出来,对照_r_0_的格式,再试两个组件看计数怎么递增。
小结
- useRef:存
{current},引用永不变。 - useMemo / useCallback:存
[值, deps],Object.is比较,变了才重算。 - useContext:就是
readContext,读栈 + 登记依赖。 - useId:计数器 + 树位置生成的稳定字符串,19.2 改下划线。
- useSyncExternalStore:
getSnapshot+subscribe的外部数据桥。 - 下一步:第 16 章看 19 的新 Hooks(
use、useOptimistic、useActionState、useFormStatus)。