react-hooks 源码简读(初识)

112 阅读5分钟

我们知道,对于class组件,我们只需要实例化一次,实例中保存了组件的state等状态。对于每一次更新只需要调用render方法就可以。但是在function组件中,每一次更新都是一次新的函数执行,为了保存一些状态,执行一些副作用钩子,react-hooks应运而生,去帮助记录组件的状态,处理一些额外的副作用。

react-hooks 是函数组件解决没有state,生命周期,逻辑不能复用的一种技术方案。

在开始学习源码之前我们先来了解一些概念,便于后面理解源码。(本文源码取自于 react 17.3.0 版本,并做了精简,下载源码对照着看效果更好。如果想看 18.3.1 版本的源码简读,可以点击(animasling.github.io/front-end-b…

current fiber树 : 当完成一次渲染之后,会产生一个current树,current会在commit阶段替换成真实的Dom树。

workInProgress fiber树: 即将调和渲染的 fiber 树。在一次新的组件更新过程中,会从current复制一份作为workInProgress,更新完毕后,将当前的workInProgress树赋值给current树。

workInProgress.memoizedState: 在class组件中,memoizedState存放state信息,在function组件中,memoizedState在一次调和渲染过程中,以链表的形式存放hooks信息。

currentHook : 可以理解 current树上的指向的当前调度的 hooks节点。

workInProgressHook: 可以理解 workInProgress树上指向的当前调度的 hooks节点。

接下来我们看下一个fiber 节点包含了哪些属性。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag; // 标记 Fiber 类型, 例如函数组件、类组件、宿主组件
  this.key = key; // 子节点的唯一键, 即我们渲染列表传入的key属性
  this.elementType = null;
  this.type = null; // 节点元素类型, 是具体的类组件、函数组件、宿主组件(字符串)
  this.stateNode = null; // 节点实例
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps; // 新的、待处理的props
this.memoizedProps = null; // 上一次渲染的props
this.updateQueue = null;
this.memoizedState = null; // 上一次渲染的组件状态
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags; // effect 标签
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null; // 指向旧树中对应节点 }

好了,现在我们了解了基础知识之后,我们现在开始直接进入主题吧。

当我们调用函数组件的时候。如果调用了react hooks, 则会执行renderWithHooks函数。

react-reconciler/src/ReactFiberBeginWork.js

function组件初始化:

renderWithHooks(
  null,                // current Fiber
  workInProgress,      // workInProgress Fiber
  Component,           // 函数组件本身
  props,               // prop
  context,             // 上下文
  renderLanes,        // 渲染 lanes
);

对于初始化是没有current树的,之后完成一次组件更新后,会把当前workInProgress树赋值给current树。
function组件更新:

renderWithHooks(
  current,
  workInProgress,
  render,
  nextProps,
  context, 
  renderLanes,
);

renderWithHooks

react-reconciler/src/ReactFiberHooks.js

通过renderWithHooks 源码,我们来看看这个函数主要做了什么。

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;
    // 1.置空即将调和渲染的workInProgress树的memoizedState和updateQueue,
    // 为什么这么做,因为在接下来的函数组件执行过程中,要把新的hooks信息挂载到这两个属性上,
    // 然后在组件commit阶段,将workInProgress树替换成current树,替换真实的DOM元素节点。并在current树保存hooks信息。
    workInProgress.memoizedState = null;
    workInProgress.updateQueue = null;
    workInProgress.lanes = NoLanes;

    // The following should have already been reset
    // currentHook = null;
    // workInProgressHook = null;
    // didScheduleRenderPhaseUpdate = false;
    // 2.判断函数组件是否是第一次渲染,然后赋予ReactCurrentDispatcher.current不同的hooks
    // TODO  注意 如果在mount时没有使用hooks,但是在update阶段用了
    // 目前我们会将 update 渲染识别为mount, 因为这个时候 memoizedState === null
    // 这个是非常棘手的,因为他对组件中的一种是有效的(e.g. React.lazy)
    // 只有在一个有状态的hook 被使用的时候才可以用momoizedState 来区分是mount 还是 update.
    // 无状态 hooks (e.g. context) 将不会添加到 memoizedState
    // 所以在updates 和 mounts 阶段,memoizedState 都有可能为 null
    ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;

    // 3.执行函数组件,hooks 被依次执行,并保存到workInProgress
    let children = Component(props, secondArg);
    // Check if there was a render phase update
    if (didScheduleRenderPhaseUpdateDuringThisPass) {
    // Keep rendering in a loop for as long as render phase updates continue to
    // be scheduled. Use a counter to prevent infinite loops.
    let numberOfReRenders: number = 0;
    do {
        didScheduleRenderPhaseUpdateDuringThisPass = false;
        invariant(
        numberOfReRenders < RE_RENDER_LIMIT,
        'Too many re-renders. React limits the number of renders to prevent ’ +
        ‘an infinite loop.’,
         );
        numberOfReRenders += 1;
        // Start over from the beginning of the list
        currentHook = null;
        workInProgressHook = null;
        workInProgress.updateQueue = null;
        ReactCurrentDispatcher.current = DEV ? HooksDispatcherOnRerenderInDEV : HooksDispatcherOnRerender; children = Component(props, secondArg); } while (didScheduleRenderPhaseUpdateDuringThisPass);
    }

    // We can assume the previous dispatcher is always this one, since we set it
    // at the beginning of the render phase and there’s no re-entrancy.
    // 4. 没有在函数组件中调用的hooks 都是ContextOnlyDispatcher 对象上的。
    ReactCurrentDispatcher.current = ContextOnlyDispatcher;
    // This check uses currentHook so that it works the same in DEV and prod bundles.
    // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
    const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;
    renderLanes = NoLanes;
    currentlyRenderingFiber = (null: any);
    currentHook = null;
    workInProgressHook = null;
    didScheduleRenderPhaseUpdate = false;
    if (enableLazyContextPropagation) {
        if (current !== null) {
            if (!checkIfWorkInProgressReceivedUpdate()) {
        // If there were no changes to props or state, we need to check if there
        // was a context change. We didn’t already do this because there’s no
        // 1:1 correspondence between dependencies and hooks. Although, because
        // there almost always is in the common case (readContext is an
        // internal API), we could compare in there. OTOH, we only hit this case
        // if everything else bails out, so on the whole it might be better to
        // keep the comparison out of the common path.
                const currentDependencies = current.dependencies;
                if (
                    currentDependencies !== null &&
                    checkIfContextChanged(currentDependencies)
                   ) {
                    markWorkInProgressReceivedUpdate();
                    }
            }
        }
    }
    return children;
}

hooks执行时,如果不在函数组件内部,则会赋予ReactCurrentDispatcher.current ContextOnlyDispatcher对象,抛出异常。

export const ContextOnlyDispatcher: Dispatcher = {
  readContext,
  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  useDebugValue: throwInvalidHookError,
  useDeferredValue: throwInvalidHookError,
  useTransition: throwInvalidHookError,
  useMutableSource: throwInvalidHookError,
  useOpaqueIdentifier: throwInvalidHookError,
  unstable_isNewReconciler: enableNewReconciler,
};
function throwInvalidHookError() {
    invariant(
    false,
    ‘Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for’ +
     one of the following reasons:\n’ +
    ‘1. You might have mismatching versions of React and the renderer (such as React DOM)\n’ +
    ‘2. You might be breaking the Rules of Hooks\n’ +
    ‘3. You might have more than one copy of React in the same app\n’ +
    ‘See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.’,
    );
}

当函数第一次渲染组件时,ReactCurrentDispatcher.current 被赋予了HooksDispatcherOnMount 对象,更新组件时赋予HooksDispatcherOnUpdate对象。

/ 第一次渲染
const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useOpaqueIdentifier: mountOpaqueIdentifier,
  unstable_isNewReconciler: enableNewReconciler,
};
// 更新渲染
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useOpaqueIdentifier: updateOpaqueIdentifier,
  unstable_isNewReconciler: enableNewReconciler,
};

我们可以通过如下流程图来总结 renderWithHook 函数主要做的事情。

好了,看到这里你应该大致了解了, 当函数执行的时候,会根据是否是初始化来调用不同的对象。下面我们将从函数初始化和更新2个方面,来学习下常用react-hooks 的主要源码。

最后推荐下我的个人网站- 【良月清秋的前端日志】(animasling.github.io/front-end-b…) ,希望我的文章对你有帮助。