React 源码系列:瞅瞅 react-reconciler 是怎么设计 hooks 的

2,173 阅读5分钟

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

今天继续学习 React 的源码,React 的仓库下有很多个模块,如 reactreact-domreact-reconcileruse-subscription等 package,这么多个模块到底从哪学起呢?

为什么想了解 react-reconciler

从目的上来讲,我想了解 React 的渲染机制、hook 是怎么设计的、修改 context 的值是怎么触发消费组件的更新的······今天我想学习一下 hook 的源码,联想到平时使用 hook 时都有这样的代码:

import { useContext, useEffect, useRef, useState } from 'react'

那么就先看一下 react 模块,果然在react源码仓库的packages/react/src/目录下找到了ReactHooks.js,但是令人失望的是,该文件的主要作用是暴露提供用户使用的 hook 方法,其中的函数实现基本都是调 dispatcher 对象的方法。通过查找函数声明和分析 import 语句,可以定位到 hook 的真正实现放在了 react-reconciler 模块的 src/ReactFiberHooks.new.js 文件夹下。

开始了解 ReactFiberHooks

这个文件有三千多行代码,我们先从 useState 开始研究。与之有关的有这几个函数:renderWithHooksmountStateupdateStatererenderState

renderWithHooks

先看一下 renderWithHooks 函数。这个函数会执行我们定义的函数式组件的渲染以及调度其中的 hook。

省略注释和非关键代码后如下所示:

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;

  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
  let children = Component(props, secondArg);
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    let numberOfReRenders: number = 0;
    do {
      didScheduleRenderPhaseUpdateDuringThisPass = false;
      localIdCounter = 0;
      if (numberOfReRenders >= RE_RENDER_LIMIT) {
        throw new Error(
          'Too many re-renders. React limits the number of renders to prevent ' +
            'an infinite loop.',
        );
      }
      numberOfReRenders += 1;
      currentHook = null;
      workInProgressHook = null;
      workInProgress.updateQueue = null;
      ReactCurrentDispatcher.current = HooksDispatcherOnRerender;
      children = Component(props, secondArg);
    } while (didScheduleRenderPhaseUpdateDuringThisPass);
  }

  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);

  currentHook = null;
  workInProgressHook = null;

  didScheduleRenderPhaseUpdate = false;

  if (didRenderTooFewHooks) {
    throw new Error(
      'Rendered fewer hooks than expected. This may be caused by an accidental ' +
        'early return statement.',
    );
  }

  return children;
}

这个函数的逻辑是在执行 let children = Component(props, secondArg);前重置 workInProgress 的 updateQueue等状态,并根据 current 这个 Fiber 是否是第一次挂载选择不同的调度器(首次挂载用 HooksDispatcherOnMount,重新渲染用HooksDispatcherOnUpdate),这启示着我们下一步可以从二者之一如 HooksDispatcherOnMount 入手研究 hook。

显然,在执行 Component 函数的时候,可能会改变 didScheduleRenderPhaseUpdateDuringThisPass,这应该是用作渲染时更新触发重渲染的标识,当它为 true 时会再次执行 Component 函数,这可能形成一个无限循环,所以这里有一个最大重渲染次数的限制,一旦超过就报错。这种报错经常出现于“在 useEffect 里面执行 setState 操作,但是忘了给 useEffect 添加依赖项”的场景。

const MyComp = () => {
  const [state, setState] = useState(null)
  useEffect(() => {
    setState(prev => prev + 1)
  })
  return <div>blabla</div>
}

还记得 react 不允许在 if 语句中使用 hook 的限制吗?一旦存在某个hook在第一次挂载被调用,在更新时没有被调用的情况,就会导致渲染后 currentHook.next !== null;,就会触发渲染 hook 太少的报错。

mountState

当我们跳转到 HooksDispatcherOnMount的定义时可以看到它给 useState字段分配的值是一个 mountState 函数。

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,

  unstable_isNewReconciler: enableNewReconciler,
};

mountState 函数

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

可以看到,我们平常使用 useState传的初始 state 被赋值给了 hook 的 memoizedState 字段,修改状态的函数 dispatch 的实现如下:

function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  const lane = requestUpdateLane(fiber);

  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    enqueueUpdate(fiber, queue, update, lane);

    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        }
      }
    }
    const eventTime = requestEventTime();
    const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
    if (root !== null) {
      entangleTransitionUpdate(root, queue, lane);
    }
  }
}

我们调用 setState 函数的入参就是这里的 action,它会作为 update对象的一部分传入enqueueRenderPhaseUpdate(queue, update);enqueueUpdate(fiber, queue, update, lane);

function enqueueRenderPhaseUpdate<S, A>(
  queue: UpdateQueue<S, A>,
  update: Update<S, A>,
) {
  didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  const pending = queue.pending;
  if (pending === null) {
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
}

可以看到,我们在开头的 renderWithHooks 函数中看到的 didScheduleRenderPhaseUpdateDuringThisPass 在这里设成了 true,这也是 setState 能够触发重渲染的原理。

hook 原理

不仅是 mountState,所有在HooksDispatcherOnMount调度器中的 mount hook 的代码实现的第一行都是 const hook = mountWorkInProgressHook(); ,这是用于注册 hook,记录 memoizedState、queue等信息。与之对应的,HooksDispatcherOnUpdate调度器中的 update hook 也有个 updateWorkInProgressHook方法。

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.

    if (nextCurrentHook === null) {
      throw new Error('Rendered more hooks than during the previous render.');
    }

    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
      // This is the first hook in the list.
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

这两个函数说明了一个组件上的所有 Hook 会组成一个单链表,挂载一个 hook 意味着创建一个链表节点,挂载在链表的尾部。在更新阶段第一次调用updateWorkInProgressHook拿到第一次挂载的hook,后面每次调用沿着链表不断取下一个 hook,如果到了尾部为空,则重新取链表头节点的 hook。为此框架才要求函数式组件中不能把hook的调用放在 if 语句中,这是为了每次调用 hook 函数能够以正确的顺序拿到hook 链表上的对应节点。

updateState

事实上,updateState 函数复用了与 useReducer 有关的updateReducer函数。

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

限于篇幅,这里不做展开,感兴趣的同学可以自行研究 react 的源码。