Skip to main content

第 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; // 原样返回同一个对象
}

updateRef2610每次渲染都返回同一个对象引用。所以两个经典行为都源于此:改 ref.current 不触发重渲染(没有 setState),以及 ref 对象在组件生命周期内身份稳定(可以当依赖数组里的"永远不变"项)。

useMemo 与 useCallback:一个 [值, deps] 缓存

两个函数几乎一样。useMemomountMemo / updateMemo):

// 更新时
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0]; // deps 没变,返回缓存的值
}
const nextValue = nextCreate(); // 变了,重新算
hook.memoizedState = [nextValue, nextDeps];

useCallbackupdateCallback)逻辑一模一样,只是"缓存的值"就是回调函数本身。所以 useCallback 是 useMemo 的简化兄弟,记住一个就记住俩。

坑也在此:areHookInputsEqual引用比较(第 14 章那个 Object.is)。deps 数组里放了个每次新建的对象,就永远"变了",缓存失效。这就是"useMemo 依赖了不该依赖的引用导致白算"的根源。

useContext:只是 readContext

useContext 在 dispatcher 里的实现就一行(ReactFiberHooks.js:3929):useContext: readContext。真正的 readContextReactFiberNewContext.js:502,它做两件事:从 context 栈里取当前值,并把这个 context 登记进 fiber 的 dependencies。登记依赖的意义在第 10 章讲过:Provider 值变了,React 能顺着 dependencies 找到这个消费者,哪怕隔着 memo。

useId:一个稳定字符串

mountId3450)按环境生成:

// 客户端
id = '_' + identifierPrefix + 'r_' + globalClientIdCounter++.toString(32) + '_';
// 服务端水合
id = '_' + identifierPrefix + 'R_' + treeId;

这就是 19 里 useId() 返回值长这样:_r_0__r_1_。19.2 从冒号改成了下划线(_:r0:__r0_),原因就是冒号在 CSS 选择器里有特殊含义。它不是随机数,是计数器 + 树位置,保证同一棵树内唯一、水合时前后端对得上。

useSyncExternalStore:给外部数据源架桥

mountSyncExternalStore1634)是 Zustand、Redux 这类库跟 React 对接的底层通道。核心是 getSnapshot() 拿值 + subscribe() 订阅变化,配合一个强制渲染检查:store 变了但快照没变就告警,防止死循环。它内部还会调用 useSyncExternalStore 自带的 useSyncExternalStore_shim 兼容旧版本。你要自己写一个订阅外部状态的 hook,它是最标准的模板。

动手实验

  1. 验证 ref 身份:组件里 const r = useRef(0); console.log(r); 连续渲染几次,确认控制台打印的是同一个对象。对照 updateRef 的"原样返回"。
  2. 看 useMemo 缓存失效useMemo(() => 计算(), [obj])obj 每次都新建,在 areHookInputsEqual 打断点,看它每次返回 false、nextCreate 每次都被调。
  3. 扒 useIduseId() 打出来,对照 _r_0_ 的格式,再试两个组件看计数怎么递增。

小结

  • useRef:存 {current},引用永不变。
  • useMemo / useCallback:存 [值, deps]Object.is 比较,变了才重算。
  • useContext:就是 readContext,读栈 + 登记依赖。
  • useId:计数器 + 树位置生成的稳定字符串,19.2 改下划线。
  • useSyncExternalStoregetSnapshot + subscribe 的外部数据桥。
  • 下一步:第 16 章看 19 的新 Hooks(useuseOptimisticuseActionStateuseFormStatus)。