跳到主要内容

第 16 章:常用 Hooks 逐个拆解

第 13 章说过,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] 缓存​

两个函数几乎一样。mountMemo(mountMemo):

function mountMemo(nextCreate, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate(); // 马上算一次
hook.memoizedState = [nextValue, nextDeps]; // 存 [值, deps]
return nextValue;
}

updateMemo(updateMemo):

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

useCallback 的实现(mountCallback / updateCallback):

// 首次
function mountCallback(callback, deps) {
const hook = mountWorkInProgressHook();
hook.memoizedState = [callback, deps]; // 存 [函数, deps]
return callback;
}
// 更新
function updateCallback(callback, deps) {
const hook = updateWorkInProgressHook();
if (areHookInputsEqual(deps, hook.memoizedState[1])) {
return hook.memoizedState[0]; // deps 没变,返回旧的函数
}
hook.memoizedState = [callback, deps]; // deps 变了,更新缓存
return callback;
}

和 useMemo 的区别只有一处:useMemo 存 nextCreate() 的返回值,useCallback 存 callback 本身。useCallback(fn, deps) 等价于 useMemo(() => fn, deps),源码层面也是同构的。

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

useContext:只是 readContext​

useContext 在 dispatcher 里的实现就一行(ReactFiberHooks.js:3929):useContext: readContext。真正的 readContext 在 ReactFiberNewContext.js:502:

function readContext(context) {
const value = context._currentValue; // 从 context 栈顶拿当前值

// 把这个 context 登记进 fiber 的 dependencies
if (lastContextDependency === null) {
lastContextDependency = {
context, next: null, memoizedValue: value,
};
currentlyRenderingFiber.dependencies = {
lanes: NoLanes,
firstContext: lastContextDependency,
};
} else {
const next = { context, next: null, memoizedValue: value };
lastContextDependency.next = next;
lastContextDependency = next; // 串成链表
}
return value;
}

它做两件事:从 context 栈里取当前值,并把这个 context 登记进 fiber 的 dependencies。登记依赖的意义在第 11 章讲过:Provider 值变了,React 能顺着 dependencies 找到这个消费者,哪怕隔着 memo。依赖也是链表(lastContextDependency 串起来),一个组件读了三个 context 就挂三个节点。

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 对接的底层通道:

function mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
const hook = mountWorkInProgressHook();
let nextSnapshot;
const isHydrating = true; // 省略水合判断
nextSnapshot = getSnapshot(); // 直接从外部 store 拿当前值

hook.memoizedState = nextSnapshot; // 存 snapshot
const inst = {
value: nextSnapshot, // 最新值
getSnapshot, // 你提供的读值函数
};
hook.queue = inst;

// effect 里注册订阅:store 变了 → forceRerender
mountEffect(
subscribeToStore.bind(null, fiber, inst, subscribe),
[subscribe],
);

// 开发环境下检查:store 变了但 snapshot 没变?
const forceRerender = dispatchSetState.bind(null, fiber, queue);
inst.forceUpdate = forceRerender;

return nextSnapshot;
}

function subscribeToStore(fiber, inst, subscribe) {
const handleStoreChange = () => {
// store 变了,检查快照是否真不同
if (checkIfSnapshotChanged(inst)) {
forceStoreRerender(fiber); // 不同就强制渲染
}
};
return subscribe(handleStoreChange); // 返回 unsubscribe
}

核心是 getSnapshot() 拿值 + subscribe() 订阅变化,配合一个强制渲染检查:store 变了但快照没变就告警,防止死循环。更新时 updateSyncExternalStore(1709)再调一次 getSnapshot(),跟 hook.memoizedState 比较,变了就触发重渲染。你要自己写一个订阅外部状态的 hook,它是最标准的模板。

动手实验​

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

小结​

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