React Hooks 原理探究

1,927 阅读4分钟

前言

React 自 16.8 推出了 Hooks,并且官方提倡使用 Hooks。本篇文章结合 React 18.2.0 的源码,以 useState 为例,探究 Hooks 的原理,有助于大家了解 React Hooks 的机制。

文章脉络:

image.png

开始

function component 运行时都会走到 react/packages/react-reconciler/src/ReactFiberHooks.old.js 文件里的 renderWithHooks 函数,本篇文章从这个函数开始:

// 去掉 dev 和精简代码
export function renderWithHooks<Props, SecondArg>(
    current: Fiber | null,
    workInProgress: Fiber,
    Component: (p: Props, arg: SecondArg) => any,
    props: Props,
    secondArg: SecondArg,
    nextRenderLanes: Lanes,
): any {

    ...
    
    // 把当前渲染的 Fiber 树存到 currentlyRenderingFiber 全局变量上, mountState 的时候会用
    currentlyRenderingFiber = workInProgress;

    // 清空
    workInProgress.memoizedState = null;
    workInProgress.updateQueue = null;
    
    ...

    // 不同阶段分配不同的调度器
    // 有些 hooks 不会在 current.memoizedState 上存信息, 比如 useContext
    // 所以在首次渲染和更新阶段 current.memoizedState 都是 null
    ReactCurrentDispatcher.current =
        current === null || current.memoizedState === null
            ? HooksDispatcherOnMount
            : HooksDispatcherOnUpdate;
    // 执行函数组件
    let children = Component(props, secondArg);
    
    ...

    currentlyRenderingFiber = (null: any);

    currentHook = null;
    workInProgressHook = null;

    ...

    return children;
}

首先分析这个函数的参数:

current:已经完成渲染的 DOM 对应的 Fiber 树。首次渲染,current 是 null;

workInProgress:即将渲染的 Fiber 树,current.alternate 指向 workInProgress,workInProgress.alternate 指向 current;

Component:函数式组件对应的函数;

renderWithHooks 函数的执行流程如下:

image.png

这个函数首先把 workInProgress 上用来保存 Hooks 信息的 memoizedState 和 updateQueue 属性置空,再给 ReactCurrentDispatcher.current 分配不同的调度器,首次渲染或者只使用了不带状态的 hooks 的组件的更新阶段分配 HooksDispatcherOnMount,否则分配 HooksDispatcherOnUpdate,再执行 Component 函数,就会依次执行函数式组件内的 Hooks,Hooks 信息会被依次保存在 workInProgress 上。

以 useState 为例,执行函数式组件内的 useState 的时候,实际是执行 react/packages/react/src/ReactHooks.js 文件里的 useState 函数:

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 获取调度器
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

useState 函数内调用的 resolveDispatcher 函数为:

// 去掉 dev
function resolveDispatcher() {
    // 该调度器为 renderWithHooks 函数内赋的值
    const dispatcher = ReactCurrentDispatcher.current;
    return ((dispatcher: any): Dispatcher);
}

可以看出 useState 函数内的 dispatcher 就是 ReactCurrentDispatcher.current,也就是 renderWithHooks 函数内对 ReactCurrentDispatcher.current 赋过值的 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate,接下来分析这两个调度器。

HooksDispatcherOnMount 调度器

首次渲染或者只使用了不带状态的 hooks 的组件的更新阶段执行的调度器是 HooksDispatcherOnMount。

const HooksDispatcherOnMount: Dispatcher = {
  ...
  // HooksDispatcherOnMount 调度器上的 useState 指向 mountState 函数
  useState: mountState,
};

mountState 函数里首先执行的是 mountWorkInProgressHook 函数,生成一个 hook 对象,先分析 mountWorkInProgressHook 函数:

function mountWorkInProgressHook(): Hook {
  // 为当前 hooks 函数创建一个 hook 对象
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  // 这里的 workInProgressHook 是上一次 hooks 函数执行产生的对应的 hook 对象
  if (workInProgressHook === null) {
    // 初始,把 hooks 链表保存在 currentlyRenderingFiber.memoizedState 上
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 再次,则接在 hooks 链表上
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

从此函数可以看出,Hooks 是按执行的顺序以单向链表的形式存储在 currentlyRenderingFiber 的 memoizedState 属性上的,从上文的 renderWithHooks 函数可以看到 currentlyRenderingFiber 是被赋值为 workInProgress 的,也就是说 Hooks 是按执行的顺序以单向链表的形式存储在 workInProgress 的 memoizedState 属性上的。

再看 mountState 函数:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  // 初始值是函数则取函数的返回值作为初始值
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  // 保存初始值
  hook.memoizedState = hook.baseState = initialState;
  
  ...
  
  // 生成一个更新 state 的函数
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

initialState 是传入 useState 的初始值,mountState 函数首先把它保存到 hook 对象的 memoizedState 属性上,再生成一个更新 state 的函数,最终返回由初始值和更新 state 的函数组成的数组。

刚刚我们以 useState 为例分析了 HooksDispatcherOnMount 调度器的执行过程,接下来分析 HooksDispatcherOnUpdate,同样以 useState 为例。

HooksDispatcherOnUpdate 调度器

使用了带状态的 hooks 的组件的更新阶段,执行的调度器是 HooksDispatcherOnUpdate。

const HooksDispatcherOnUpdate: Dispatcher = {
  ...
  // useState 执行的时候实际是调用 updateState 函数
  useState: updateState,
};

updateState 函数:

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

updateState 函数里复用了跟 useReducer 相关的 updateReducer 函数。

// 去掉 dev
function updateReducer<S, I, A>(
    reducer: (S, A) => S,
    initialArg: I,
    init?: I => S,
): [S, Dispatch<A>] {
    // 获取当前要更新的 hook 信息
    const hook = updateWorkInProgressHook();
    // 更新队列
    const queue = hook.queue;

    ...

    const current: Hook = (currentHook: any);
    let baseQueue = current.baseQueue;
    // 将上一次更新的 pending queue 合并到 baseQueue,代码已省略
    if (baseQueue !== null) {
        // baseQueue 是闭环链表
        const first = baseQueue.next;
        // 旧状态
        let newState = current.baseState;
        let update = first;
        // 循环计算 newState
        do {
            const action = update.action;
            newState = reducer(newState, action);
            update = update.next;
        } while (update !== null && update !== first);
        // 值不同时标记 fiber 变化
        if (!is(newState, hook.memoizedState)) {
            markWorkInProgressReceivedUpdate();
        }
        // 赋新值
        hook.memoizedState = newState;
        hook.baseState = newBaseState;
        hook.baseQueue = newBaseQueueLast;
        queue.lastRenderedState = newState;
    }
    // 把优先级不够的 update 放到下一次更新时处理,代码已省略
    const dispatch: Dispatch<A> = (queue.dispatch: any);
    // 返回由最新的 state 和更新 state 的函数组成的数组
    return [hook.memoizedState, dispatch];
}

总结

以上,从 renderWithHooks 函数开始,以 useState 为例,分两种情况分析了 HooksDispatcherOnMount 和 HooksDispatcherOnUpdate 这两个调度器的执行流程,帮助我们更好地了解 Hooks。