React 原理系列 —— Hook 是这样工作的

语言: CN / TW / HK

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

Part 1 函数式组件和 Hook

通常情况下,我们在函数式组件中这样调用 hook:

function Example() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>add</button>
    </div>
  );
}

函数式组件本身是个纯函数,没有任何状态,它通过调用 useState 获取一个状态和改变状态的方法。但这个 useState 是 React 导出的“全局”函数,和当前函数式组件没有任何显式关联。所以必定有某种力量将一个函数式组件实例和它用到的 state 绑定起来。

Fiber 上的 Hook

这个力量就是 Fiber,在 Fiber 节点中有两个相关属性:

  1. type:指向 Component,可能是 Function Component,也可能是 Class Component 等其他组件。对 Function Component 来说,就是一个具体的 render 函数,比如上面的 Example 函数。
  2. memoizedState:指向自身状态,在 Class Fiber 下是构造函数声明的 state,在 Function Fiber 下则是一个 Hook 池。Hook 池中维护着组件调用 useXXX 产生的所有 Hook,Hook 中又分别记着各自的状态。这样就实现了 Hook 和 Fiber 的绑定。
Hook 池和 Fiber 绑定

Hook 池和 Fiber 绑定的意义在于,当某个 Function Component 的 Fiber 开始 render,它能根据状态池定位到上一次 render 的 Hook 状态,为本次 render 执行的所有 useXXX 提供行为依据,“一次性”函数就有了“延续”的状态。

找到“上一次”

任意一个 Function Component Fiber 更新时都会走到 renderWithHooks 方法:

function updateFunctionComponent( current, workInProgress, Component, nextProps, renderExpirationTime ) {
  nextChildren = renderWithHooks(current,workInProgress,Component, nextProps,context,renderExpirationTime);
}

这个方法在 ReactFiberHooks 模块中,模块里有全局的 nextCurrentHook 指针,表明当前指向的 Hook。renderWithHooks 会首先切换 nextCurrentHook 到当前 Fiber 的 Hook 池,再执行 render 函数,然后 render 函数中调用的所有“全局”useXXX 都从这个指针获取“上一次”。

切换 nextCurrentHook
function renderWithHooks(current,workInProgress,Component,props,refOrContext,nextRenderExpirationTime) {
  // 切换 nextCurrentHook
  nextCurrentHook = current !== null ? current.memoizedState : null;
  // 执行 render 方法
  let children = Component(props, refOrContext);
  return children;
}

弄清楚 Hook 和 Fiber 的绑定和切换,接下来我们进到一个 Fiber 节点内部,看看 Hook 池的维护机制。

Part 2 Hook 池的维护机制

Hook 机制的所有实现,都在前面提到的 ReactFiberHooks 模块中。

Hook 的数据结构

Hook 是一个对象,render 中调用 useXXX 方法,就会创建一个 Hook 对象。

type Hook = {
  memoizedState: any;
  baseState: any;
  baseUpdate: Update<any, any> | null;
  queue: UpdateQueue<any, any> | null;
  next: Hook | null;
}

如果你调用的方法不一样,Hook 对象里面的字段搭载的信息也不一样。比如 useState、useReducer 这样的 State Hook,和 useEffect 这样的 Effect Hook,就会在 memoizedState 上存不同的东西。

当组件调用多次 useXXX,就会创建多个 Hook。同一 Component 的多个 Hook 之间用链表连接起来,构成 Hook 池,Fiber 的 memoizedState 就指向池中第一个 Hook:

Hook 池结构

除了 nextCurrentHook,ReactFiberHooks 提供了一些其他指针来做遍历:

let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
let nextWorkInProgressHook: Hook | null = null;

前两个指针用于遍历已有的 Hook 池,后三个指针用来构建一个新 Hook 池。

方法换档

虽然看起来 Function Component 每次 render 都调用的同一个 useXXX,但实际上 mount 和 update 调用对 Hook 池是几乎完全不同的操作。

因此 ReactFiberHooks 提供了一个换档机制:声明两套 HooksDispatcher,上面绑定了 mount、update 阶段不同的 Hook 实现。当一个 Function Component 准备 render 时,判断它是 mount 还是 update,切换不同的 HooksDispatcher。

换挡机制

具体的换挡逻辑仍在 renderWithHooks 中,而对 mount 还是 update 的判断,则依赖当前 Fiber 是否有 Hook 池:

nextCurrentHook = current !== null ? current.memoizedState : null;
ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

这里其实不合理,如果组件没用到 Hook,即使在 update 阶段,也不会有 Hook 池。但也无所谓,反正发动机没转,换哪个档都不会动。既然能换挡,我们也就能看到不同阶段下,Hook 池的构建和读取方式。

Hook 池的构建

Fiber mount 阶段,Fiber 上没有 Hook 池,从头构建:

Hook 池的构建
  1. 构造新 Hook。
  2. 接入链表。如果 Fiber 上没有 Hook,说明当前 Hook 是整个函数式组件的第一个 Hook,放 firstWorkInProgressHook 最后接造 Fiber 上;如果有 Hook,直接 next 接下去。
  3. 更新指针。

这部分逻辑抽象在每个 mountXXX 都要调的 mountWorkInProgressHook 中。

function mountWorkInProgressHook(): Hook {
  // 1. 构造新 Hook
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null,
  };
  // 2. 接入链表;3. 更新指针
  if (workInProgressHook === null) {
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

mountWorkInProgressHook 返回创建的 Hook,随后不同的 mountXXX 会再对 Hook 做各自的处理,后面分开说。

等整个 Hook 池构建完毕,在 renderWithHooks 中挂到 Fiber 的 memoizedState 上:

// renderWithHooks 方法
renderedWork.memoizedState = firstWorkInProgressHook;

Hook 池的更新

Fiber 开始 update,通过 renderWithHooks 读取 Fiber.memoizedState 上的第一个 Hook,并给到 nextCurrentHook 指针。

nextCurrentHook = current !== null ? current.memoizedState : null;

然后每个逐个进入 updateXXX。我们以第一个 Hook 操作为例,会调通用的 updateWorkInProgressHook 方法执行以下操作:

  1. 克隆当前 Hook。为什么不直接复用?这样可以保证 Fiber 上的 Hook “原件”完整,某些情况下(比如 useEffect 要对比新旧 Hook 的依赖),在构建当前 Hook 的同时,仍需要上一次 Hook 的信息。
  2. 更新指针。沿着 next 把“当前 Hook”指针指向链表的下一次节点,同时 workInProgressHook 也通过 next 构建下去。这就是网红问题 “为什么 Hook 不能用在 if / for 语句里?” 的答案,一旦中间某次 useXXX 没 next,会导致后续所有 Hook 取错。

附部分代码:

function updateWorkInProgressHook() {
  currentHook = nextCurrentHook;
  // 1. 克隆 Hook
  const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    queue: currentHook.queue,
    baseUpdate: currentHook.baseUpdate,
    next: null,
  };
  // 2. 更新指针
  if (workInProgressHook === null) {
    workInProgressHook = firstWorkInProgressHook = newHook;
  } else {
    workInProgressHook = workInProgressHook.next = newHook;
  }
  nextCurrentHook = currentHook.next;
  return workInProgressHook;
}

等 Hook 池更新完毕,renderWithHooks 同样会把 Fiber 的 memoizedState 切换到 firstWorkInProgressHook。

小结

这节介绍了 Hook 的基本机制:

  • 函数内的 Hook 调用创建一个 Hook 对象,上面保存着状态数据。
  • Hook 维护在一个 Hook 池中,并挂到 Fiber 节点上,Hook 池是一个单向链表。
  • Fiber 在 mount 和 update 阶段通过“换挡”切换 dispatcher,调用不同的 useXXX 实现。
  • mount 阶段要构建 Hook 池;update 阶段则逐个克隆 Hook,构建新的 Hook 池。

Part 3 State Hook:提供状态

State Hook 来自 useState、useReducer,用来给函数式组件提供状态,它的实现围绕状态及其更新。

const [ state, dispatch ] = useState(initial);

State Hook 结构

在 State Hook 中,memoizedState 保存着 Hook 的最新状态,baseState 则是初始状态(useState 传入)。queue 保存着这个 hook 的更新队列,数据结构为单向循环链表:

State Hook 构建:mount 阶段

State Hook 经过通用的构造阶段,执行自己的逻辑,我们补充到下图中:

  • 状态初始赋值。也就是我们调用 useState 的入参。
  • 构造更新队列,绑定 dispatch。更新队列会在需要更新的时候被“清洗”来计算最新 memoizedState,而 dispatch 是修改队列的唯一入口。
  • 返回 state 和 dispatch。这样我们第一次调用 useState 完成,获得 Hook 的初始状态值和更新状态的方法。

附代码:

function mountState() {
  const hook = mountWorkInProgressHook();
  // 4. 状态初始赋值
  hook.memoizedState = hook.baseState = initialState;
  // 5. 构造更新队列
  const queue = (hook.queue = { last, dispatch, lastRenderedReducer, lastRenderedState });
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  )));
  // 6. 返回 state 和 dispatch
  return [hook.memoizedState, dispatch];
}

dispatchAction 在 mount 阶段绑定到 Hook 上并返回,后续的更新直接来 Hook 上调就好。

State Hook 读取:update 阶段

Fiber update 阶段,useState 从 Hook 池中克隆出 Hook,获取历史状态,计算新状态。以第一个 Hook 操作为例,补齐后续几步:

  • 计算最新 state。这是个清理 Update Queue 的动作,可选,如果两次 render 之间没有对这个 Hook 节点的 set 操作,就不会有 Update,可以直接返回现有 state。
  • 返回最新 state 和 dispatch。

附部分代码:

function updateReducer() {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  // 3. 合并 queue,计算最新 memoizedState
  let newState = hook.memoizedState;
  let update = firstRenderPhaseUpdate;
  do {
    const action = update.action;
    newState = reducer(newState, action);
    update = update.next;
  } while (update !== null);
  hook.memoizedState = newState;
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  // 4. 
  return [hook.memoizedState, dispatch];
}

State Hook 更新:dispatchAction

dispatchAction 是第一次调 useState 时返回出去的,作为 setXXX 调用。那很显然,setXXX 把传入的更新加入 Hook 更新队列,并触发一次更新。这里借用之前那篇的图:( React Fiber 架构 —— “更新”到底是个啥 - 知乎

很简单的插入链表操作,完成后通过 scheduleWork 发起更新。

代码:

function dispatchAction<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) {
  // 1. 构造更新
  const update: Update<S, A> = {
    expirationTime,
    action,
    eagerReducer: null,
    eagerState: null,
    next: null,
  };
  // 2. 插入队列
  const last = queue.last;
  if (last === null) {
    update.next = update;
  } else {
    const first = last.next;
    if (first !== null) {
      update.next = first;
    }
    last.next = update;
  }
  queue.last = update;
  // 3. 发起调度
  scheduleWork(fiber, expirationTime);
}

小结

这节介绍了 Stete Hook 的实现:

  • State Hook 来自 useState、useReducer,用来提供状态及其更新。
  • State Hook 通过 memoizedState 保存状态,通过 queue 维护更新队列的数据和方法(dispatch)。
  • State Hook 的更新队列是个单向循环链表。
  • 更新阶段的 State Hook 会“清洗”更新队列,计算并返回最新 memoizedState。
  • 我们调用 useState 返回的 dispatch,就是创建并在更新队列中插入新更新,并发起整体调度。

Part 4 Effect Hook:依赖监听和清理

Effect Hook 来自 useEffect、useLayoutEffect,为函数式组件提供依赖监听。

useEffect(() => {
    // create Effect
  return () => {
    // destroy Effect
  };
}, [deps]);

Effect Hook 结构

在 Effect Hook 上,只有 memoizedState 被用到,用来存储一个 Effect 对象。这个对象会被插入 Fiber 的更新队列,告诉 Fiber 更新后要做哪些动作。

type Effect = {
  tag: HookEffectTag,
  create: () => (() => void) | void,
  destroy: (() => void) | void,
  deps: Array<mixed> | null,
  next: Effect,
};
  • create:就是我们传入的第一个回调函数,在依赖变化的时候执行
  • destroy:用来记录 create 执行后的返回函数。某些阶段来自于上一次同一个 Effect Hook 的 create 执行结果,某些阶段来自于自身。
  • deps:就是我们传入的依赖数组,用来进行依赖对比。
  • tag:决定 Effect 在 Fiber 提交时如何被处理。这个很关键,它的值来自于 useEffect 的执行时机和依赖变化情况。

Effect Hook 构建和更新

第一次渲染,走到 mountEffect,通过通用 mountWorkInProgressHook 构造并插入一个 Hook 返回。然后对这个 Hook 做一些操作,我们关注 4、5、6 步:

  • 构造 Effect(副作用对象)。记录了副作用的依赖数组、回调函数、清理函数、处理方式。
  • Effect 加入 Fiber 节点的 updateQueue。updateQueue 是个链表,链表元素就是我们构造的 Effect,这个链表会在 Fiber 更新完毕后逐个根据 Effect 对象提供的信息处理和清空。
  • Effect 同时也会挂到当前 Hook 上。

构建代码:

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {...};
  const lastEffect = componentUpdateQueue.lastEffect;
  if (lastEffect === null) {
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const firstEffect = lastEffect.next;
    lastEffect.next = effect;
    effect.next = firstEffect;
    componentUpdateQueue.lastEffect = effect;
  }
  return effect;
}

更新执行函数式组件时,每个 useEffect 又会被分别执行一次,同样要分别克隆 Hook,然后构造 Effect 挂到 updateQueue 和 Hook 上。

  • 对比旧 Hook,确定 Effect 对象的属性。这是因为 Effect 的内容要依赖前面的 Effect,比如销毁函数(destroy)就是由上一次执行创建函数(create)返回的。下一节我们展开看。
  • 构造 Effect
  • Effect 入队
  • Effect 挂到当前 Hook 上

代码:

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  if (currentHook !== null) {
    // 4. 对比旧 Hook,确定 Effect 对象的属性
    // ...暂时省略,后面展开说
  }
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}

至此,无论 Fiber 初始化还是更新,我们所有 useEffect 产生的 Effect,都可以从 Hook 池和 Update Queue 访问到。其中 Hook 池方便我们管理所有 Hook(包括之前 State Hook)的数据,Effect 就是 Effect Hook 的数据;Update Queue 则为了方便 Fiber 直接拿到 Effect 执行副作用。

Effect 属性的取值和流转

接下来我们看看 Effect 在构造过程中,在不同场景下,是如何根据用户传参、Effect 之间关系,来确定自身属性的。

Effect 由 pushEffect 函数创建,传入的四个参数直接赋值给对应属性:

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
  };
  // ...
}

pushEffect 的调用入参,直接决定了 Effect 的内容。

初始化创建 Effect

在 mountEffectImpl 中,是对 Effect 的初始化创建: pushEffect(hookEffectTag, create, undefined, nextDeps) ,对应属性传值如下:

  • tag:也就是 hookEffectTag,值为 mountEffect 传入的 UnmountPassive | MountPassive => 0b10000000 | 0b01000000 => 0b11000000
  • create:useEffect 的第一个入参函数
  • destroy:传入一个 undefined,因为初始化创建的 Effect 不存在“前一次执行”
  • deps:依赖数组

Effect 被执行

然后初始化创建的 Effect 会在 Fiber commit 的时候被执行,在 commitWork 阶段一个叫 commitHookEffectList 的方法中,对 Effect 做了这样的动作:

const create = effect.create;
effect.destroy = create();

这时 Effect 的 destroy 被「暂时」挂上了自己 create 的返回。这就是前面说的「某些阶段来自于上一次同一个 Effect Hook 的 create 执行结果,某些阶段来自于自身。」

但这样不会乱吗?上一次 destroy 不就找不到了吗?不会。因为 Effect 总是先执行上一次 destroy,再执行自己的 create,此时上一次 destroy 已经执行过没用了,正好空出来 destroy 属性挂自己的,方便传给下一次 Effect。

更新创建 Effect

Fiber 更新阶段再执行 useEffect,来到 updateEffectImpl。这里首先会去拿上一次构造的 Effect,再看这张图:

新 Hook 被克隆出来,上一次 Hook 则留在 currentHook 指针上,通过 currentHook.memoizedState 拿到它的 Effect,以及Effect 的依赖(deps)和销毁函数(destroy,按前面所说,此时得到的 destroy 就是上一次 Effect 自己的)。

接着会做一件重要的事: 依赖对比 ,依赖是否变化,对当前 Effect 的处理方式影响很大。

依赖变化是怎么对比出来的?遍历 + 浅比较。

// areHookInputsEqual 方法
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
  if (is(nextDeps[i], prevDeps[i])) {
    continue;
  }
  return false;
}
return true;

// is 方法
return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);

当依赖有变化,创建的 Effect 属性值如下:

  • tag:也就是 hookEffectTag,值为 updateEffect 传入的 UnmountPassive | MountPassive => 0b10000000 | 0b01000000 => 0b11000000
  • create:useEffect 的第一个入参函数
  • destroy:上一次 Effect 的 destroy
  • deps:依赖数组

当依赖没变化,Effect tag 会变成 NoHookEffect => 0b00000000

小结

至此我们摸清了在被 Fiber commit 处理前,Update Queue 中的 Effect 属性来源:

  • tag:需要处理的时候(mount 或依赖有变的 update)为 0b11000000 ,不需要处理的时候(依赖不变的 update)为 0b00000000
  • create:useEffect 的第一个入参函数
  • destroy:上一次 Effect 的 destroy
  • deps:依赖数组

处理 Fiber 上的 Effect

useEffect 产生的所有 Effect 都加入 Fiber 的 Update Queue,由 Fiber 在 commit 阶段统一处理。

在 commitRoot 入口,对 FunctionComponent 这样调用 commitHookEffectList 方法:

commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
commitHookEffectList(NoHookEffect, MountPassive, finishedWork);

其中前两个参数是常量 tag,用来和 Effect tag 比较判断 Effect 的处理方式,对应方法参数 unmountTag、mountTag;第三个参数 finishedWork 传入当前 Fiber。commitHookEffectList 被先后调了两次,从传参来看,NoHookEffect 是 0b00000000 置空,两次的 UnmountPassive(0b10000000) 和 MountPassive(0b01000000) 分别激活 unmountTag、mountTag,依次执行 umount 和 mount 操作。

commitHookEffectList 是统一处理 Update Queue 中 Effect 的入口,再回顾下 Update Queue 的结构:

队列里第一个 Effect 可以通过 const firstEffect = finishedWork.updateQueue.lastEffect.next 拿到,然后按照单向循环链表遍历:

// 遍历 Update Queue
do {
  // 处理 Effect
  effect = effect.next;
} while (effect !== firstEffect);

Unmount 处理

处理 Effect 的方式判断很巧妙,通过一个二进制位运算: (effect.tag & unmountTag) !== NoHookEffect ,判断是否需要进行 umount 处理。NoHookEffect 是个 0,那就要 effect.tag 和 unmountTag 不同且都不为 0,条件判断才为 true。结合 Effect 的属性值和 commitHookEffectList 传参内容,我们列举出以下几种情况:

  • Effect 依赖没变(effect.tag = 0),则肯定不做 unmount 处理
  • 第二次 commitHookEffectList(unmountTag = 0),也肯定不做 ummount 处理
  • Effect 依赖有变(effect.tag = 0b11000000),且第一次 commitHookEffectList(unmountTag = 0b10000000),则做 unmount 处理:执行 effect.destroy(上一次 Effect 的销毁函数)
if ((effect.tag & unmountTag) !== NoHookEffect) {
  const destroy = effect.destroy;
  effect.destroy = undefined;
  if (destroy !== undefined) {
    destroy();
  }
}

Mount处理

也通过二进制来做: (effect.tag & mountTag) !== NoHookEffect

  • Effect 依赖没变(effect.tag = 0),则肯定不做 mount 处理
  • 第一次 commitHookEffectList(mountTag = 0),也肯定不做 mount 处理
  • Effect 依赖有变(effect.tag = 0b11000000),且第二次 commitHookEffectList(mountTag = 0b01000000),则做 mount 处理:执行 effect.create(回调 useEffect 入参,并把返回作为 destroy 挂到 Effect 上)
if ((effect.tag & mountTag) !== NoHookEffect) {
  const create = effect.create;
  effect.destroy = create();
}

小结

这节介绍了 Effect Hook 实现:

  • Effect Hook 来自 useEffect、useLayoutEffect。
  • Effect Hook 通过 memoizedState 保存一个 useEffect 产生的 Effect 对象。
  • Effect 对象保存着创建(create)回调、销毁(destroy)回调、依赖、处理标记。
  • Effect 会同时挂到 Fiber 的 Update Queue 上,方便 Fiber 在 commit 阶段找到并执行,Update Queue 是个单向循环链表。
  • 更新阶段,Effect 会找到同一调用在上一次构建的 Effect,对比依赖以决定被如何处理,并获取 destroy。

Part 5 其他 Hook

经过前面对 Hook 池和两种关键 Hook 的介绍,我们基本摸清了 Hook 的套路:调用 useXXX —> 构建 Hook 维护 Hook 池 —> 在 Hook 上加一些不同 useXXX 特有的数据和逻辑。以此类推,就很容易猜到其他 Hook 的实现方式了。

useMemo

useMemo 返回一个memoized 值。把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Memo Hook 上要存什么状态?依赖值和触发的计算值,实现上是一个数组。然后把计算值返回出去:

// mountMemo 和 updateMemo 方法
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;

但在 update 阶段,上述逻辑是有条件的,即“依赖有变化”,否则只要返回上一次计算值就好。所以 useEffect 曾用到的 areHookInputsEqual 又出场了:

// updateMemo 方法
const prevState = hook.memoizedState;
if (areHookInputsEqual(nextDeps, prevDeps)) {
  return prevState[0];
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;

useCallback

同 useMemo 十分相似,不同点是入参的方法不执行,变为直接存储:

// mountMemo 和 updateMemo 方法
hook.memoizedState = [callback, nextDeps];
return nextValue;

useRef

useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”,而这个“盒子”的引用值始终不变。

只要在 mount 时创建一个对象,存到 Hook 上,后续 update 直接取:

// mountRef
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;

// updateRef
return hook.memoizedState;

Part Z 总结

本篇介绍了 Hook 的实现原理:

  • Function Component 中每次调用 useXXX,都会创建一个 Hook,这些 Hook 以 Hook 池的形式维护在 Fiber.memoizedState 上。
  • Hook 是一个对象,上面存着对应调用的数据。不同 useXXX 方法在 Hook 上存的东西也不同。
  • Function 初次执行和后续更新执行,Hook 池的维护方式、useXXX 要做的事大不相同,所以 React 会判断当前 Fiber 所处阶段(mount 或 update),换挡调用不同的 useXXX 实现。
  • Fiber mount 时,Hook 被依次从头创建;update 时则逐个从旧 Hook 池中克隆,构造成新 Hook 池后再切换 Fiber.memoizedState,这样能保留旧 Hook,提供“新旧 Hook 对比”的能力。
  • State Hook(useState、useReducer)在 Hook 对象上保存状态值和更新队列,update 阶段会清理更新队列并计算最新状态值。useState、useReducer 返回的 dispatch 方法用来把更新加入队列,并发起一次更新调度。
  • Effect Hook(useEffect)在 Hook 对象上保存 Effect,一个记录副作用创建、销毁、依赖的对象。update 阶段构建新的 Effect,并对比新老 Hook 上 Effect 的依赖,决定 Effect 处理方式。Effect 会被同时记录在 Fiber 自身的更新队列里,等待 Fiber commit 后统一处理。