likes
comments
collection
share

React Hooks 原理分析

作者站长头像
站长
· 阅读数 17

React Hooks 原理分析

hooks 出现的原因

我们知道在React V16.8版本之前React组件主要由类组件和无状态组件组成,而两者之间存在明显的差异:类组件可以使用状态(State)和生命周期钩子(Lifecycle Hook),但不能使用无状态(Functional)组件的优点——易于使用和定义。在React项目开发中,类组件不仅仅难于理解和阅读,而且操作和维护也很困难,代码量比较大和代码重复。而无状态组件优点在于简单明了,易于测试和重复使用,但又缺乏状态处理的能力。因此为了弥补无状态组件没有生命周期,没有数据管理状态的缺陷,React Hooks就应运而生了。

什么是 hooks 以及 hooks 解决了什么问题。

React 16.8版本中Hooks作为React的一种全新特性,它可以让开发者在不编写类组件的情况下,使用 React 的一系列功能。Hooks 是以函数组件为基础开发的,它使函数式组件具有类组件一样的能力,包括状态管理、生命周期钩子和副作用等。使用函数式组件代替类组件解决了以下几个问题。

  • 原来类组件的状态难以复用,必须使用高阶组件(HOC)等方式进行解决,导致代码复杂。使用Hooks 以后,可以通过自定义 Hooks 将状态送进封装,不再需要使用HOC等方式进行复用。
  • 类组件的生命周期方法繁杂,必须写多个生命周期函数才能完成一些操作。使用 Hooks 后,可以根据需要使用不同的Hooks来替代某些生命周期函数,比如使用 useEffect 来替代componentDidMount 等生命周方法。
  • 类组件中的this指向问题必须时刻关注着。在函数组中,没有this和生命周方法的概念,也不必使用bind()来绑定 this,这样代码更简单明了。

hooks 原理与执行时机

从我上篇 一文读懂react Fiber 可知,在Render阶段会从rootFiber节点开始向下深度优先遍历,每个fiber节点调用 beginWork 方法,其中摘录项目代码如下:

function beginWork(current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const updateLanes = workInProgress.lanes;
   // 根据tag类型不同执行不同 action
  switch (workInProgress.tag) {
    // .......
    case FunctionComponent: {
      // .....
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
  }
}

这里我们重点看FunctionComponent类型下执行了updateFunctionComponent方法,通过点击查看 源码 ,同样我们取其中有关代码如下:

function updateFunctionComponent(
      current,
      workInProgress,
      Component,
      nextProps: any,
      renderLanes,
    ) {
      let context;
      let nextChildren;
      prepareToReadContext(workInProgress, renderLanes);
      // 调用renderWithHooks方法
      nextChildren = renderWithHooks(
          current,
          workInProgress,
          Component,
          nextProps,
          context,
          renderLanes,
        );
      // ......
      // return workInProgress.child;
   }

通过查看updateFunctionComponent方法,我们知道该方法最终调用了renderWithHooks,下面我们来看看 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;
      
      // 重置 workInProgress 节点中的 memoizedState 等状态信息
      workInProgress.memoizedState = null;
      workInProgress.updateQueue = null;
      workInProgress.lanes = NoLanes;
      // 设置当前 dispatcher,根据 current 和 memoizedState 判断是初次渲染或者更新调用不同的 dispatcher
      ReactCurrentDispatcher.current =
          current === null || current.memoizedState === null
            ? HooksDispatcherOnMount
            : HooksDispatcherOnUpdate;
      
      // 调用Component函数执行函数组件
      let children = Component(props, secondArg);

      // 检查RenderPhase, 是否是render阶段触发的更新,防止无限循环重复更新。
      if (didScheduleRenderPhaseUpdateDuringThisPass) {
       // .....;
      }
      // 设置 React 当前的 dispatcher 为只包含上下文的 dispatcher。
      ReactCurrentDispatcher.current = ContextOnlyDispatcher;
      // 当前渲染状态
      const didRenderTooFewHooks =
        currentHook !== null && currentHook.next !== null;

      // 重置相关参数
      renderLanes = NoLanes;
      currentlyRenderingFiber = (null: any);
      currentHook = null;
      workInProgressHook = null;
      didScheduleRenderPhaseUpdate = false;

      return children;
 }

该方法接受如下几个参数:

  • current:当前fiber节点,初始化时为null
  • workInProgress:正在处理的fiber节点
  • Component:函数组件本身
  • props:组件属性
  • secondArg:组件属性以外的其他参数,可以是null或者省略
  • nextRenderLanes:渲染优先级

实现逻辑大致如下:

  • 保存渲染所需的状态信息,包括当前渲染Fiber节点(currentlyRenderingFiber)、渲染优先级(renderLanes)、Hooks链表(currentHook、workInProgressHook)、是否进行过RenderPhase更新(didScheduleRenderPhaseUpdate)等。
  • 重置workInProgress节点中的状态信息,并根据当前状态设置React当前的dispatcher(当前正在处理哪个优先级的任务)。
  • 调用待渲染组件的函数,并将其结果保存在children变量中。
  • 判断是否需要进行后续的渲染操作(如出现了RenderPhase更新)。
  • 设置React当前的dispatcher,处理只包含上下文的dispatcher
  • 根据当前状态判断是否渲染了全部Hook函数。
  • 重置渲染所需的状态信息,以准备进行下一次的渲染。
  • 返回执行结果

需要注意的是:

  1. 这里通过判断current树上是否存在memoizedState信息来确认是初次渲染还是更新,以便调用不同的Dispatcher
    // 判断 mount 与 update
    current === null || current.memoizedState === null
    // mount时的Dispatcher
    const HooksDispatcherOnMount: Dispatcher = {
      readContext,

      useCallback: mountCallback,
      useContext: readContext,
      useEffect: mountEffect,
      useImperativeHandle: mountImperativeHandle,
      useLayoutEffect: mountLayoutEffect,
      useMemo: mountMemo,
      useReducer: mountReducer,
      useRef: mountRef,
      useState: mountState,
      // ......
    };
     // update时的Dispatcher
     const HooksDispatcherOnUpdate: Dispatcher = {
      readContext,

      useCallback: updateCallback,
      useContext: readContext,
      useEffect: updateEffect,
      useImperativeHandle: updateImperativeHandle,
      useLayoutEffect: updateLayoutEffect,
      useMemo: updateMemo,
      useReducer: updateReducer,
      useRef: updateRef,
      useState: updateState,
      // ....
    };

表明不同的调用栈上下文为ReactCurrentDispatcher.current赋值不同的dispatcher,函数组件 render 时调用的hook也是不同的函数。当然在React中还有其他dispatcher,如有兴趣可以点击 这里 查看。

2. 其次这里通过调用Component函数来执行函数组件,函数组件在这里被正式执行。 3. 通过ContextOnlyDispatcher对象来判断hooks是否包含在上下文里面,否则抛出异常。以错误调用两个Hook为例:

useEffect(() => {
   useState(1);
},[])

此时ContextOnlyDispatcher 格式如下:

const ContextOnlyDispatcher = {
    useState:throwInvalidHookError,
    useEffect: throwInvalidHookError,
    // ....
}
function throwInvalidHookError() {
    // ......
  invariant(
    //....
  );
 }

这时的ReactCurrentDispatcher.current已经指向ContextOnlyDispatcher,所以调用useState实际会调用throwInvalidHookError,直接抛出异常。

接下来我们来分别看下函数组件mount时和update时不同阶段内部数据结构和具体流程。分析这两个阶段之前我们先了解一下hook数据结构,源码点击 这里

const hook: Hook = {
  memoizedState: null, // 单一hook对应的数据

  baseState: null, // 最新state值
  baseQueue: null, // 基础更新队列
  queue: null, // 待更新队列

  next: null, // 指向下一个hooks指针对象
};

这里需要注意的是memoizedState属性,不同的hook类型保存不同的数据信息。

  • useStateuseReducermemoizedState保存的为state的值。
  • useEffectmemoizedState保存的 useEffect回调函数依赖项等的链表数据结构effect
  • useReducer:格式如const [state, dispatch] = useReducer(reducer, {});memoizedState保存state的值。
  • useRefmemoizedState中保存的{current: xxx}
  • useMemo:格式如useMemo(callback, dep[A])中, memoizedState保存的是[callback(), depA]
  • useCallback:格式如useCallback(callback, dep[A])memoizedState保存[callback, depA]。与useMemo的区别:useCallback保存的是回调函数本身,而useMemo保存的是回调函数执行的结果。
  • useContextmemoizedState属性。

注意: FunctionComponent fiber也存在memoizedState属性,两者不能混淆。 fiber.memoizedStateFunctionComponent对应fiber保存的Hooks链表。 hook.memoizedStateHooks链表中保存的单一hook对应的数据。

接下来我们分别看看几个常用的API(useStateuseEffectuseReduceruseRefuseMeomouseCallback)在mount阶段update阶段不同的处理流程。

常用API不同阶段处理流程

在分析常用API之前我们得知每个API在初始化阶段都会调用 mountWorkInProgressHook 函数,在更新阶段会调用 updateWorkInProgressHook 函数,下面我们先分别来看看这两个函数做了什么。

  • mountWorkInProgressHook

    function mountWorkInProgressHook(): Hook {
        // 创建hook对象
        const hook: Hook = {
          memoizedState: null,
    
          baseState: null,
          baseQueue: null,
          queue: null,
    
          next: null,
        };
         // 通过判断workInProgressHook是否存在来设置hook对象数据
        if (workInProgressHook === null) {
          // 设置当前正在渲染的fiber节点memoizedState属性并给hook对象赋值
          currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
        } else {
          // hook对象添加到链表的末尾,同时设置workInProgressHook和hook对象的next属性
          workInProgressHook = workInProgressHook.next = hook;
        }
        return workInProgressHook;
     }
    

    该函数的功能是创建一个 hook 对象,并将其添加到 Fiber 节点的链表中,用于记录组件状态或执行副作用。如果链表中没有任何 hook对象,则将其添加为第一个hook 对象;如果已经存在其他hook对象,则将其添加到链表的末尾。函数返回创建的hook对象。具体的hook对象属性代表的含义我们上述已经提前说过了。

  • updateWorkInProgressHook

    function updateWorkInProgressHook(): Hook {
        // 下一个当前hook指针
        let nextCurrentHook: null | Hook;
        // 是否是第一个hook
        if (currentHook === null) {
        const current = currentlyRenderingFiber.alternate;
        if (current !== null) {
          nextCurrentHook = current.memoizedState;
        } else {
          nextCurrentHook = null;
        }
      } else {
        // 指向下一个hook
        nextCurrentHook = currentHook.next;
      }
      // 下一个nextWorkInProgressHook
      let nextWorkInProgressHook: null | Hook;
      if (workInProgressHook === null) {
        // 当前渲染节点的memoizedState
        nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
      } else {
        // 赋值当前workInProgressHook链表中的下一个hook
        nextWorkInProgressHook = workInProgressHook.next;
      }
      //是否存在 nextWorkInProgressHook
      if (nextWorkInProgressHook !== null) {
        // 存在则直接复用,将当前workInProgressHook指向下一个hook
        workInProgressHook = nextWorkInProgressHook;
        nextWorkInProgressHook = workInProgressHook.next;
        // currentHook 指向下一个hook
        currentHook = nextCurrentHook;
      } else {
        // 克隆新hook对象
        currentHook = nextCurrentHook;
    
        const newHook: Hook = {
          memoizedState: currentHook.memoizedState,
    
          baseState: currentHook.baseState,
          baseQueue: currentHook.baseQueue,
          queue: currentHook.queue,
    
          next: null,
        };
        // 是否是链表的第一个hook
        if (workInProgressHook === null) {
          currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
        } else {
          // 将新hook添加到workInProgressHook链表中
          workInProgressHook = workInProgressHook.next = newHook;
        }
      }
      // 返回
      return workInProgressHook;
    }
    

    总的来说,这个函数的主要作用是为当前进行的fiber节点创建一个workInProgressHook,并返回该hook对象。具体实现逻辑如下:

    1. 定义了变量 nextCurrentHook 用于存储下一个当前 hook 的指针,其中如果当前 hook 不为 null,则将其指向下一个 hook,否则将它指向当前正在渲染的 fiber节点的备用节点上的 memoizedStatelastRenderedState)。
    2. 定义了变量 nextWorkInProgressHook 用于存储下一个workInProgressHook,其中如果 workInProgressHook 不为 null,则将其指向当前 workInProgressHook 链表中的下一个 hook,否则将其指向当前 fiber节点的第一个 hook 对象(memoizedState)。
    3. 如果下一个 workInProgressHook 已经存在,则直接复用它,并将 nextCurrentHookworkInProgressHook 的指针指向下一个 hook。如果不存在下一个 workInProgressHook,则需要从当前 hook 对象克隆一个新的 hook 对象,并添加到 workInProgressHook 链表的末尾。如果这是链表中的第一个 hook,则将其赋值给 currentlyRenderingFiber.memoizedState
    4. 最后将 workInProgressHook 对象返回,以供下一次使用。如果没有发生错误,则 workInProgressHook 对象就会包含渲染或更新过程中要使用的所有 hook 对象。

useState

源码如下:

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 当前环境的dispatcher,不通的场景获取不同的dispatch对象
  const dispatcher = resolveDispatcher();
  // 当前的函数组件中创建一个新的状态,并返回一个数组对象
  return dispatcher.useState(initialState);
}
  • mount

    mount时对于useState,会调用 mountState 方法。

     function mountState<S>(
          initialState: (() => S) | S,
        ): [S, Dispatch<BasicStateAction<S>>] {
           // 创建当前hook对象
          const hook = mountWorkInProgressHook();
          if (typeof initialState === 'function') {
            // 第一次参数为函数时执行函数得到返回值
            initialState = initialState();
          }
          hook.memoizedState = hook.baseState = initialState;
          // 创建queue
          const queue = (hook.queue = {
            pending: null,
            dispatch: null,
            lastRenderedReducer: basicStateReducer, // 最新state
            lastRenderedState: (initialState: any), // 最后一次的state
          });
          // 创建更新函数并返回
          const dispatch: Dispatch<
            BasicStateAction<S>,
          > = (queue.dispatch = (dispatchAction.bind(
            null,
            currentlyRenderingFiber,
            queue,
          ): any));
          return [hook.memoizedState, dispatch];
        }
    

    初始化时,useState会首先获取当前的hook对象,然后根据传入的initialState,设置hook对象memoizedStatebaseState为初始值,同时为hook对象创建更新队列queue

  • update

    update时会调用 updateState 方法。

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

    阅读该方法发现方法内部会调用updateReducer方法,接下来我们看看updateReducer方法内部做了什么,源码如下:

    function updateReducer<S, I, A>(
      reducer: (S, A) => S, // 纯函数
      initialArg: I, // 初始值
      init?: I => S, // 可选参数
    ): [S, Dispatch<A>] {
      const hook = updateWorkInProgressHook(); // 创建更新hook对象
      const queue = hook.queue; // 更新队列
      // 处理错误 ......
      // 将当前reducer标记为lastRenderedReducer
      queue.lastRenderedReducer = reducer;
      const current: Hook = (currentHook: any);
      // 基础更新队列信息
      let baseQueue = current.baseQueue;
    
      // 当前待处理的更新队列
      const pendingQueue = queue.pending;
      if (pendingQueue !== null) {
        if (baseQueue !== null) {
          // 与当前baseQueue进行合并为一个新的基础队列
          const baseFirst = baseQueue.next;
          const pendingFirst = pendingQueue.next;
          baseQueue.next = pendingFirst;
          pendingQueue.next = baseFirst;
        }
        // .......
        current.baseQueue = baseQueue = pendingQueue;
        queue.pending = null;
      }
    
      // 处理基础对列
      if (baseQueue !== null) {
        // 第一个更新对象
        const first = baseQueue.next;
        // 当前组件的基础状态
        let newState = current.baseState;
        // 初始化新基础状态值
        let newBaseState = null;
        let newBaseQueueFirst = null;
        let newBaseQueueLast = null;
        let update = first;
        // 遍历对列中的每个更新对象
        do {
          // 获取对象值
          const suspenseConfig = update.suspenseConfig;
          const updateLane = update.lane; // 优先级
          const updateEventTime = update.eventTime;
          if (!isSubsetOfLanes(renderLanes, updateLane)) {
             // 创建clone对象,里面包含一些属性值
            const clone: Update<S, A> = {
              // ......
            };
            if (newBaseQueueLast === null) {
              newBaseQueueFirst = newBaseQueueLast = clone;
              newBaseState = newState;
            } else {
              newBaseQueueLast = newBaseQueueLast.next = clone;
            }
            currentlyRenderingFiber.lanes = mergeLanes(
              currentlyRenderingFiber.lanes,
              updateLane,
            );
            // 标记被跳过的更新对象的优先级
            markSkippedUpdateLanes(updateLane);
          } else {
            //  新的基础状态对列中有更新对象
            if (newBaseQueueLast !== null) {
              // 创建副本
              const clone: Update<S, A> = {
               // ........
              };
              // 将当前更新对象追加到新队列尾部
              newBaseQueueLast = newBaseQueueLast.next = clone;
            }
            // 标记更新对象的 eventTime 和 suspenseConfig 属性
            markRenderEventTimeAndConfig(updateEventTime, suspenseConfig);
            // 更新对象的 eagerReducer 属性等于当前的 reducer 函数,直接赋值
            if (update.eagerReducer === reducer) {
              newState = ((update.eagerState: any): S);
            } else {
              // reducer函数计算新状态
              const action = update.action;
              newState = reducer(newState, action);
            }
          }
          // 下一个更新对象
          update = update.next;
          // 如果 update 不为空且不等于第一个更新对象,则继续进行循环
        } while (update !== null && update !== first);
    
        if (newBaseQueueLast === null) {
          newBaseState = newState;
        } else {
          // 新基础状态队列的首元素链接到队列尾部
          newBaseQueueLast.next = (newBaseQueueFirst: any);
        }
        // 比较新状态与memoizedState
        if (!is(newState, hook.memoizedState)) {
          markWorkInProgressReceivedUpdate();
        }
        // 更新hook对象的属性值
        hook.memoizedState = newState;
        hook.baseState = newBaseState;
        hook.baseQueue = newBaseQueueLast;
        queue.lastRenderedState = newState;
      }
      // 更新函数
      const dispatch: Dispatch<A> = (queue.dispatch: any);
      // 最后返回memoizedState值与dispatch函数
      return [hook.memoizedState, dispatch];
    }
    

    源码比较长,但仔细阅读发现该函数主要是实现hooks状态更新的逻辑。该函数接收一个reducer函数和一个初始化参数initArg,以及可选的初始化参数init,并返回当前memoized状态和一个dispatch函数。具体实现过程如下:

    • 创建当前更新Hook对象,再从中取出更新队列queue。将当前 reducer 标记为 lastRenderedReducer。获取当前 HookmemoizedState,如果它是 null,则为 Hook 创建一个初始状态,并将其更新为 memoizedState。如果eagerReducer存在并且与当前使用的 reducer 相同,则使用 eagerState 来更新 currentState
    • 接下来是状态更的过程:获取当前基础状态队列 baseQueue,并检查是否存在等待处理的更新队列 pendingQueue。如果存在,则将其与 baseQueue 合并为一个新的基础状态队列。
    • 遍历更新队列中的每个更新,根据其更新优先级及有关状态来判断更新是否是需要被处理。如果更新优先级过低,则将其标记为跳过,并将一个相对应的更新对象放入到新的基础状态队列中。如果更新优先级足够高,则直接将其应用于 currentState,以便更新 memoizedState
    • 当更新队列中有更新时,将 memoizedState 的值更新为最新的值,同时将hook对象的基础状态(baseState)和基础状态更新队列(baseState updateQueue)属性也更新为新的状态队列。
    • 最后返回memoizedState值以及dispatch函数。

useReducer

源码如下:

export function useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 获取当前环境的dispatcher
  const dispatcher = resolveDispatcher();
  // 返回当前状态和用于更新状态的函数
  return dispatcher.useReducer(reducer, initialArg, init);
}
  • mount

    mount时对于useReducer,会调用 mountReducer 方法。

    function mountReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      // 创建当前hook对象
      const hook = mountWorkInProgressHook();
      let initialState;
      // 赋值初始state
      if (init !== undefined) {
        initialState = init(initialArg);
      } else {
        initialState = ((initialArg: any): S);
      }
      hook.memoizedState = hook.baseState = initialState;
      // 创建queue
      const queue = (hook.queue = {
        pending: null,
        dispatch: null,
        lastRenderedReducer: reducer,
        lastRenderedState: (initialState: any),
      });
      // 创建dispatch更新函数并返回
      const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
        null,
        currentlyRenderingFiber,
        queue,
      ): any));
      return [hook.memoizedState, dispatch];
    }
    

    阅读源码可以发现mount时,useStateuseReducer 这两个hook的唯一区别为queue参数的lastRenderedReducer字段。其中queue的数据结构如下:

    const queue = (hook.queue = {
      pending: null,
      // 保存dispatchAction.bind()的值
      dispatch: null,
      // 上一次render时使用的reducer
      lastRenderedReducer: reducer,
      // 上一次render时的state
      lastRenderedState: (initialState: any),
    });
    

    其中,useReducerlastRenderedReducer为传入的reducer参数。useStatelastRenderedReducerbasicStateReducerbasicStateReducer方法源码如下:

    function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
      return typeof action === 'function' ? action(state) : action;
    }
    

    可见,useStatereducer参数为basicStateReduceruseReducer

  • update

    update时对于useReducer则和useState更新时调用的是同一个updateReducer函数,上面已经介绍过了,这里就不做介绍了。

通过以上分析我们知道对于useStateuseReducer这两个hook都是通过 dispatchAction 函数来触发更新的。把FunctionComponent对应的fiber以及hook.queue通过调用bind方法作为参数传入,精简如下

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  // 更新相关信息 
  const eventTime = requestEventTime();
  const suspenseConfig = requestCurrentSuspenseConfig();
  const lane = requestUpdateLane(fiber, suspenseConfig);

  // 创建update对象
  const update: Update<S, A> = {
    eventTime,
    lane,
    suspenseConfig,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

  // 添加新的更新对象到更新队列中
  const pending = queue.pending;
  if (pending === null) {
    // 队列中没有更新,新的更新对象为唯一更新对象,构建循环列表
    update.next = update;
  } else {
    // 已有更新对象,插入到更新队列到最后
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
  // 处理是否需要重新渲染
  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // 当前节点正在渲染,则标记有新的更新任务
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  } else {
    if (
      // 当前 fiber 节点及其备份节点暂无待执行的更新任务
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // 获取队列上一次渲染时所使用的reducer
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        try {
          // 获取队列上一次渲染时的 state
          const currentState: S = (queue.lastRenderedState: any);
          // 根据新的 action 计算 eagerState
          const eagerState = lastRenderedReducer(currentState, action);
          // 存储到新的更新对象上
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
          // 对比eagerState 与当前 state 是否相同,相同不需要做额外的操作
            return;
          }
        }
      }
    }
    // 当前 fiber 节点暂无可执行的任务,更新任务
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}

以上代码实现流程大致如下:

  • 获取事件时间、当前suspense配置、待更新fiber节点更新队列UpdateQueue、优先级用来创建一个更新对象并添加到队列的末尾。
  • 判断当前 fiber节点是否正在被渲染或者其备份节点正在被渲染。如果是的话,标记 didScheduleRenderPhaseUpdateDuringThisPassdidScheduleRenderPhaseUpdate true
  • 如果不是第一次更新且当前 fiber节点没有可用的更新优先级(lane),并且其备份节点也没有可用的更新优先级,则判断队列中最后一次渲染的reducer是否存在,如果有则尝试调用该 reducer 生成 eagerState,并将其与当前状态比较,如果两者相同则不需要更新。
  • 如果以上条件都不满足,则调用 scheduleUpdateOnFiber 函数,将当前更新任务推到任务队列中等待执行。

useRef

源码如下:

export function useRef<T>(initialValue: T): {|current: T|} {
  const dispatcher = resolveDispatcher();
  // 返回带有current属性的对象
  return dispatcher.useRef(initialValue);
}
  • mount

    mount时对于useRef,会调用 mountRef 方法,源码如下:

    function mountRef<T>(initialValue: T): {|current: T|} {
      // 创建当前 useRef hook
      const hook = mountWorkInProgressHook();
      // 创建 ref
      const ref = {current: initialValue};
      // .....
      // 保存到hook对象的memoizedState属性上
      hook.memoizedState = ref;
      return ref;
    }
    

    mountRef源码非常简单,也是创建一个当前hook对象,然后创建一个ref对象,其中ref对象current属性用来保存初始化值,保存在hook对象memoizedState属性上,最后返回这个ref对象

  • update

    update时对于useRef,会调用 updateRef 方法。源码如下:

    function updateRef<T>(initialValue: T): {|current: T|} {
      const hook = updateWorkInProgressHook();
      // 返回缓存值
      return hook.memoizedState;
    }
    

    updateRef函数方法内部同样简单,函数接受一个泛型参数initialValue,返回缓存值。其中hook.memoizedState在内存中指向了一个带有current属性的对象,无论函数组件执行多少次,总能访问到最新值。

useMemo

源码如下:

export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  // memoized版本的值。根据deps是否改变重新计算缓存值
  return dispatcher.useMemo(create, deps);
}
  • mount

    mount时对于useMemo,会调用 mountMemo 方法。

    function mountMemo<T>(
      nextCreate: () => T,
      deps: Array<mixed> | void | null,
    ): T {
      // 创建当前hook对象
      const hook = mountWorkInProgressHook();
      // 判断依赖数组
      const nextDeps = deps === undefined ? null : deps;
      // 调用函数创建memoized值
      const nextValue = nextCreate();
      hook.memoizedState = [nextValue, nextDeps];
      return nextValue;
    }
    

    该方法接受一个创建函数和依赖数组作为参数,创建一个memoized值并将该值保存在hook.memoizedState中,最后返回该值。其中通过比较依赖数组的值来减少组件重复渲染的次数,以达到提高性能的目的。

  • update

    update时对于useMemo,会调用 updateMemo 方法。

    function updateMemo<T>(
      nextCreate: () => T,
      deps: Array<mixed> | void | null,
    ): T {
      const hook = updateWorkInProgressHook();
      // 获取新的依赖值
      const nextDeps = deps === undefined ? null : deps;
      // 获取上次保存的memoized值
      const prevState = hook.memoizedState;
      if (prevState !== null) {
        // memoized值已经存在,根据依赖数组的变化情况来判断是否需要更新memoized值。
        if (nextDeps !== null) {
          // 获取之前保存的deps值
          const prevDeps: Array<mixed> | null = prevState[1];
          // 判断两次获取的依赖值
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            // 直接返回上次保存的memoized值
            return prevState[0];
          }
        }
      }
      const nextValue = nextCreate();
      // 更新 memoized 值
      hook.memoizedState = [nextValue, nextDeps];
      return nextValue;
    }
    

    该方法逻辑其实和mountMemo方法逻辑类似,不同点在于它需要判断memoized值是否需要更新,以及更新时如何处理。都是用于创建memoized值,提高组件的性能和效率。

useCallback

源码如下:

export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  // memoized版本的回调函数,在 `deps` 数组中的某一项发生变化时才进行更新。
  return dispatcher.useCallback(callback, deps);
}
  • mount

    mount时对于useCallback,会调用 mountCallback 方法。

    function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
      const hook = mountWorkInProgressHook();
      // 依赖数组
      const nextDeps = deps === undefined ? null : deps;
      // 赋值
      hook.memoizedState = [callback, nextDeps];
      // 返回回调函数
      return callback;
    }
    

    可以看到mount时,useMemouseCallback的逻辑基本一样,唯一的区别在于:mountMemo会将回调函数(nextCreate)的执行结果作为value保存。而mountCallback会将回调函数作为value保存。

  • update

    update时对于useCallback,会调用 updateCallback 方法。

    function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
      const hook = updateWorkInProgressHook();
      // 获取依赖值
      const nextDeps = deps === undefined ? null : deps;
      const prevState = hook.memoizedState;
      // memoized值是否存在
      if (prevState !== null) {
        if (nextDeps !== null) {
          const prevDeps: Array<mixed> | null = prevState[1];
          // 判断两次获取的依赖值
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            // 直接返回上次保存的值
            return prevState[0];
          }
        }
      }
      // 更新 memoized 值
      hook.memoizedState = [callback, nextDeps];
      return callback;
    }
    

    可见,对于update时,useMemouseCallback的唯一区别也是是回调函数本身还是回调函数的执行结果作为value

useEffect

源码如下:

export function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  // 根据deps是否改变处理副作用(异步请求、DOM操作)
  return dispatcher.useEffect(create, deps);
}
  • mount

    mount时对于useEffect,会调用 mountEffect 方法。

    function mountEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null,
    ): void {
      if (__DEV__) {
        // ......
      }
      return mountEffectImpl(
        UpdateEffect | PassiveEffect,
        HookPassive,
        create,
        deps,
      );
    }
    
    function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
      // 创建当前 hook 对象
      const hook = mountWorkInProgressHook();
      // 获取依赖值
      const nextDeps = deps === undefined ? null : deps;
      // 根据参数 fiberEffectTag 和 hookEffectTag 计算出当前 effect 的 effectTag(副作用标记)并存储到当前渲染 fiber 节点上。
      currentlyRenderingFiber.effectTag |= fiberEffectTag;
      // pushEffect 用于创建副作用对象,以便后续渲染时被处理,并执行相关的副作用函数以及清除函数
      hook.memoizedState = pushEffect(
        HookHasEffect | hookEffectTag,
        create, // 副作用函数
        undefined,
        nextDeps, // 依赖数组deps
      );
    }
    

    通过以上源码可知mountEffect方法,最终调用了mountEffectImpl方法。mountEffectImpl方法内部都会创建一个hook对象。然后将该hook对象memoizedState属性上保存当前副作用对象信息,以便后续渲染时被处理,并执行相关的副作用函数以及清除函数。其中pushEffect方法用于创建副作用对象,根据组件是否初次渲染挂载到workInProgressupdateQueue属性上。然后将副作用(effect)放入updateQueue中,组成一个effect list。感兴趣的可以点击 这里 查看源码。

  • update

    update时对于useEffect,会调用 updateEffect 方法。

    function updateEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null,
    ): void {
      if (__DEV__) {
        // ......
      }
      return updateEffectImpl(
        UpdateEffect | PassiveEffect,
        HookPassive,
        create,
        deps,
      );
    }
    
     function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
      const hook = updateWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      let destroy = undefined;
       // 根据currentHook是否为空来判断上次渲染过程中是否使用了useEffect hook
      if (currentHook !== null) {
        const prevEffect = currentHook.memoizedState;
        // 赋值上次渲染时的清除函数
        destroy = prevEffect.destroy;
        if (nextDeps !== null) {
          const prevDeps = prevEffect.deps;
          // 两次deps是否相等,memoized值无需更新
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            pushEffect(hookEffectTag, create, destroy, nextDeps);
            return;
          }
        }
      }
    
      currentlyRenderingFiber.effectTag |= fiberEffectTag;
    
      hook.memoizedState = pushEffect(
        HookHasEffect | hookEffectTag,
        create,
        destroy,
        nextDeps,
      );
    }
    

    从以上代码分析updateEffect方法最终会调用updateEffectImpl方法,与 mountEffectImpl函数类似。该方法内部会根据两次的deps是否相等来决定此次更新是否需要执行。如果不相等,则更新effect并且重新赋值hook.memoizedState

effect list

上面我们提到了effect list概念,接下来我们简要分析下effect list的创建和更新过程:

  • 当我们在函数组件内使用 useEffect()useLayoutEffect()hook 时,函数组件初次 render时,React 会调用mountEffect()函数来创建 effect list

    1. mountEffect() 中,使用 createEffect() 函数创建一个副作用对象,包括 effectTagcreate(副作用函数)、destroy(清除函数)和依赖数组等信息。
    2. 副作用对象会被添加到一个单向链表中,而这个链表实际上就是 effect list
  • 当我们使用 useEffect()useLayoutEffect()hook 时,如果传入的依赖数组发生了变化,函数组件会re-renderReact 会调用 updateEffect() 函数来更新 effect list

    1. updateEffect() 中,React 首先获取到上次 render 时保存的副作用对象(即 memoizedState),然后比较当前的依赖数组是否等于上次的依赖数组。如果依赖数组没有变化,React 会跳过这个副作用对象。
    2. 如果依赖数组发生了变化,React 会调用 createEffect() 函数来创建一个新的副作用对象,并将其添加到 effect list 中。
  • 在组件render之后,React会执行effect list中保存的所有副作用。

    1. commit 阶段React 遍历 effect list,并根据 effectTag 的不同,执行对应的副作用:

      • 如果 effectTag 包含 Unmount,则表示该副作用需要在组件卸载之前执行。
      • 如果effectTag 包含 Layout,则表示该副作用需要在 commit 阶段同步执行。
      • 如果 effectTag 包含 Passive,则表示该副作用是在浏览器空闲时异步执行。
    2. 在执行副作用时,React 会调用副作用函数createcreate函数即是我们自定义传入 useEffect()useLayoutEffect() 的函数。副作用函数可能会返回一个清除函数(即 destroy 函数),这个清除函数会在如下情况下被调用

      • 组件 unmount 时;
      • 下一次 commit 时,因为组件 re-render 导致有新的副作用产生;
      • 当前副作用链表中,出现一个或多个 effectTag 包含一个 Update 标志位的副作用,意味着它引用了上一个 render 输出中的某些值,在当前 render 输出中可能失效。

简单来说 effect listReact 中用于存储组件副作用的一种数据结构。它是一个数组,存储了当前组件所有需要执行的副作用。React 通过 effect list 统一管理组件的副作用信息,并在需要执行副作用时,通过 effectTag 判断执行时机,确保副作用的正确性。在组件初次 mount re-render 时,React 会创建和更新 effect list。在执行副作用时,React 会调用副作用函数 create,并根据返回值中是否包含清除函数 destroy,来判断该副作用是否需要清理和再次执行。

为什么不能在条件语句中调用hook

以下面代码为例:

    import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react';
    import {Button} from 'antd';
    const App = () => {
        const [num, setNum] = useState(0);
        const divRef = useRef(null);
        const memoValue = useMemo(() => 'i am jackbtone', []);
        const memoizedFunc = useCallback(() => {
            console.log('func 函数被执行')
        }, [num])
        useEffct(() => {
            console.log('组件开始执行......')
        }, [])
        return <>
            <div ref={divRef}>
                <span>{num}</span>
                {memoValue}
                <Button onClick{() => setNum(num + 1)}>{num}</Button>
                <Button onClick{() => memoizedFunc()}>触发</Button>
            </div>
        </>
    }

当函数组件被执行之后,我们通过以上分析可知,上面的五个hook被依次执行,执行各自hook对应的dispatch后会形成如下图所示的链表关系。

React Hooks 原理分析 从上图可以看出在函数组件执行后,调用各自hookdispatch执行mountWorkInProgressHook 方法后,形成了一个完整的hooks链表,通过next指针指向下一个hook

假设我们在上面的代码中加入条件循环。

   if(isMount) {
       const memoValue = useMemo(() => 'i am jackbtone', []);
   }

此时通过一次更新后会出现如下图所示

React Hooks 原理分析

通过上图我们可以知道在组件更新时,hooks链表的结构发生了破坏,current树memoizedState值与workInProgress存储的memoizedState值不一致,导致next指针指向下一个hook的信息出错,此时涉及到存取State值的操作就会发生意想不到的结果。

使用 hooks 需遵循以下几个原则

  • 只能在函数的顶层作用域调用 Hooks,不能在套函数里调用。
  • 必须在 React 的函数组件或者自定义的 Hook函数中调用。
  • 所有的 Hooks函数在每个渲染周期都会按相同的顺序执行,不能使用条件语句来改变它们之间的顺序,否则会导致组件状态逻辑的混乱和不可预测性。
  • Hooks 的命名必须以 use 开头,这是为了让 React 能够正确检测使用的钩子。

参考文章

React源码

React Hooks原理