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

1,166 阅读17分钟

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;

编辑切换为居中

添加图片注释,不超过 140 字(可选)

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

编辑切换为居中

添加图片注释,不超过 140 字(可选)

  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。

编辑切换为居中

添加图片注释,不超过 140 字(可选)

小结

这节介绍了 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 的更新队列,数据结构为单向循环链表:

编辑切换为居中

添加图片注释,不超过 140 字(可选)

State Hook 构建:mount 阶段

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

编辑切换为居中

添加图片注释,不超过 140 字(可选)

  • 状态初始赋值。也就是我们调用 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 操作为例,补齐后续几步:

编辑切换为居中

添加图片注释,不超过 140 字(可选)

  • 计算最新 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 架构 —— “更新”到底是个啥 - 知乎

编辑切换为居中

添加图片注释,不超过 140 字(可选)

很简单的插入链表操作,完成后通过 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 步:

编辑切换为居中

添加图片注释,不超过 140 字(可选)

  • 构造 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 上。

编辑切换为居中

添加图片注释,不超过 140 字(可选)

  • 对比旧 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,再看这张图:

编辑切换为居中

添加图片注释,不超过 140 字(可选)

新 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 的结构:

编辑切换为居中

添加图片注释,不超过 140 字(可选)

队列里第一个 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 后统一处理。