React 深度学习:ReactFiberHooks

2,131 阅读16分钟

path:packages/react-reconciler/src/ReactFiberHooks.js

类型定义

Dispatcher 分发器

export type Dispatcher = {
  readContext<T>(
    context: ReactContext<T>,
    observedBits: void | number | boolean,
  ): T,
  useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
  useReducer<S, I, A>(
    reducer: (S, A) => S,
    initialArg: I,
    init?: (I) => S,
  ): [S, Dispatch<A>],
  useContext<T>(
    context: ReactContext<T>,
    observedBits: void | number | boolean,
  ): T,
  useRef<T>(initialValue: T): {current: T},
  useEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  useLayoutEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
  useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
  useImperativeHandle<T>(
    ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
    create: () => T,
    deps: Array<mixed> | void | null,
  ): void,
  useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
};

Dispatcher 类型定义了 10 个 hook 方法的基本形式,并额外增加一个读取上下文的方法 readContext

Hooks 类型

export type HookType =
  | 'useState'
  | 'useReducer'
  | 'useContext'
  | 'useRef'
  | 'useEffect'
  | 'useLayoutEffect'
  | 'useCallback'
  | 'useMemo'
  | 'useImperativeHandle'
  | 'useDebugValue';

Update、UpdateQueue、Effect

type Update<S, A> = {
 expirationTime: ExpirationTime,
 action: A,
 eagerReducer: ((S, A) => S) | null,
 eagerState: S | null,
 next: Update<S, A> | null,
};

// 更新队列
type UpdateQueue<S, A> = {
 last: Update<S, A> | null,
 dispatch: (A => mixed) | null,
 lastRenderedReducer: ((S, A) => S) | null,
 lastRenderedState: S | null,
};

type Effect = {
 tag: HookEffectTag,
 create: () => (() => void) | void,
 destroy: (() => void) | void,
 deps: Array<mixed> | null,
 next: Effect,
};

export type FunctionComponentUpdateQueue = {
 lastEffect: Effect | null,
};

type BasicStateAction<S> = (S => S) | S;

type Dispatch<A> = A => void;

Hook

export type Hook = {
 memoizedState: any,

 baseState: any,
 baseUpdate: Update<any, any> | null,
 queue: UpdateQueue<any, any> | null,

 next: Hook | null,
};

renderWithHooks

React 深度学习:ReactHooks 一文中我已经看到,最重要的部分是 ReactCurrentDispatcher 对象,它有一个 current 属性,这个属性是 Dispatcher 类型的对象。但到目前我们还没有见到这个对象的初始化,或者叫对象的具体实现。

通读 ReactFiberHooks.js 的代码,发现有几个方法中有对 ReactCurrentDispatcher.current 赋值的代码:

  • renderWithHooks
  • resetHooks
  • dispatchAction

当然,为了严谨,我用 IDEA 查找了所有对 ReactCurrentDispatcher.current 的引用,未发现其他有使用到的地方。

renderWithHooks

export function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime,
): any {
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;
  nextCurrentHook = current !== null ? current.memoizedState : null;

  if (__DEV__) {
    // do something
  }

  // The following should have already been reset
  // currentHook = null;
  // workInProgressHook = null;

  // remainingExpirationTime = NoWork;
  // componentUpdateQueue = null;

  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;
  // sideEffectTag = 0;

  // TODO Warn if no hooks are used at all during mount, then some are used during update.
  // Currently we will identify the update render as a mount because nextCurrentHook === null.
  // This is tricky because it's valid for certain types of components (e.g. React.lazy)

  // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
  // Non-stateful hooks (e.g. context) don't get added to memoizedState,
  // so nextCurrentHook would be null during updates and mounts.
  if (__DEV__) {
    // do something
  } else {
    ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }

  let children = Component(props, refOrContext);

  if (didScheduleRenderPhaseUpdate) {
    do {
      didScheduleRenderPhaseUpdate = false;
      numberOfReRenders += 1;

      // 从列表的开头重新开始
      nextCurrentHook = current !== null ? current.memoizedState : null;
      nextWorkInProgressHook = firstWorkInProgressHook;

      currentHook = null;
      workInProgressHook = null;
      componentUpdateQueue = null;

      if (__DEV__) {
        // do something
      }

      ReactCurrentDispatcher.current = __DEV__
        ? HooksDispatcherOnUpdateInDEV
        : HooksDispatcherOnUpdate;

      children = Component(props, refOrContext);
    } while (didScheduleRenderPhaseUpdate);

    renderPhaseUpdates = null;
    numberOfReRenders = 0;
  }

  // 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.
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const renderedWork: Fiber = (currentlyRenderingFiber: any);

  renderedWork.memoizedState = firstWorkInProgressHook;
  renderedWork.expirationTime = remainingExpirationTime;
  renderedWork.updateQueue = (componentUpdateQueue: any);
  renderedWork.effectTag |= sideEffectTag;

  if (__DEV__) {
    // do something
  }

  // 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;

  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  currentHook = null;
  nextCurrentHook = null;
  firstWorkInProgressHook = null;
  workInProgressHook = null;
  nextWorkInProgressHook = null;

  if (__DEV__) {
    // do something
  }

  remainingExpirationTime = NoWork;
  componentUpdateQueue = null;
  sideEffectTag = 0;

  // These were reset above
  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;

  invariant(
    !didRenderTooFewHooks,
    'Rendered fewer hooks than expected. This may be caused by an accidental ' +
      'early return statement.',
  );

  return children;
}

上面的代码中有有两处赋值:

ReactCurrentDispatcher.current = __DEV__
        ? HooksDispatcherOnUpdateInDEV
        : HooksDispatcherOnUpdate;

ReactCurrentDispatcher.current = ContextOnlyDispatcher;

resetHooks

export function resetHooks(): void {
  // 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.
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  // This is used to reset the state of this module when a component throws.
  // It's also called inside mountIndeterminateComponent if we determine the
  // component is a module-style component.
  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  currentHook = null;
  nextCurrentHook = null;
  firstWorkInProgressHook = null;
  workInProgressHook = null;
  nextWorkInProgressHook = null;

  if (__DEV__) {
    // do something
  }

  remainingExpirationTime = NoWork;
  componentUpdateQueue = null;
  sideEffectTag = 0;

  didScheduleRenderPhaseUpdate = false;
  renderPhaseUpdates = null;
  numberOfReRenders = 0;
}
ReactCurrentDispatcher.current = ContextOnlyDispatcher;

dispatchAction

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  invariant(
    numberOfReRenders < RE_RENDER_LIMIT,
    'Too many re-renders. React limits the number of renders to prevent ' +
      'an infinite loop.',
  );

  if (__DEV__) {
    // do something
  }

  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // This is a render phase update. Stash it in a lazily-created map of
    // queue -> linked list of updates. After this render pass, we'll restart
    // and apply the stashed updates on top of the work-in-progress hook.
    didScheduleRenderPhaseUpdate = true;
    const update: Update<S, A> = {
      expirationTime: renderExpirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    flushPassiveEffects();

    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update: Update<S, A> = {
      expirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };

    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;

    if (
      fiber.expirationTime === NoWork &&
      (alternate === null || alternate.expirationTime === NoWork)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        if (__DEV__) {
          prevDispatcher = ReactCurrentDispatcher.current;
          ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactCurrentDispatcher.current = prevDispatcher;
          }
        }
      }
    }
    if (__DEV__) {
      // do something
    }
    scheduleWork(fiber, expirationTime);
  }
}
if (__DEV__) {
  prevDispatcher = ReactCurrentDispatcher.current;
  ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}

if (__DEV__) {
    ReactCurrentDispatcher.current = prevDispatcher;
}

但这都是开发环境的逻辑,不作过去阐述。

在上面三个方法中,我们看到 ReactCurrentDispatcher.current 分被赋予了如下值:

  • HooksDispatcherOnMount

  • HooksDispatcherOnUpdate

  • ContextOnlyDispatcher

  • HooksDispatcherOnUpdateInDEV

  • InvalidNestedHooksDispatcherOnUpdateInDEV

HooksDispatcherOnMount

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
};

该 hook dispatcher 在组件挂在时被使用,其中的每一个方法都有对应的实现。

HooksDispatcherOnUpdate

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

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

该 hook dispatcher 在组件更新时被使用,其中的每一个方法都有对应的实现。

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,
};
  • throwInvalidHookError 方法:

    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://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
        );
    }
    

    该方法直接抛出了一个错误,错误信息说明这是一个无效的 hook 调用,原因:

    • hook 只能在函数组件的主体内部调用
    • 有 React 版本和呈现器(如React DOM) 不匹配
    • 可能违反了 hook 的规则
    • 可能在同一个应用程序中有多个 React 副本

通过以上信息我们不难看出,ContextOnlyDispatcher 不过错误调用信息的载体,它暴露同样的方法以能够在错误地调用的钩子时能正确回退。

其他限定于 DEV 的 HOOK

React 还有其他一些只用于开发环境的 hook dispatcher,他们不过是在 HooksDispatcherOnMountHooksDispatcherOnUpdate 中的方法上包装了一层,用以进行限定于开发环境的额外操作。

有兴趣的同学可以自己去翻翻源码。

Hook 方法

useState

  • mount

    function mountState<S>(
      initialState: (() => S) | S,
    ): [S, Dispatch<BasicStateAction<S>>] {
      const hook = mountWorkInProgressHook();
      if (typeof initialState === 'function') {
        initialState = initialState();
      }
      hook.memoizedState = hook.baseState = initialState;
      const queue = (hook.queue = {
        last: null,
        dispatch: null,
        lastRenderedReducer: basicStateReducer,
        lastRenderedState: (initialState: any),
      });
      const dispatch: Dispatch<
        BasicStateAction<S>,
      > = (queue.dispatch = (dispatchAction.bind(
        null,
        // Flow doesn't know this is non-null, but we do.
        ((currentlyRenderingFiber: any): Fiber),
        queue,
      ): any));
      return [hook.memoizedState, dispatch];
    }
    
    

    从上面的代码我们可以看到:

    • useStateinitialState 参数可以是一个函数,这一点是官方文档没有讲到的

    • useState 的返回值是一个数组,第一个元素为当前值;第二个置为一个将前两位参数限定为当前 Fiber 和当前更新队列 queue 的一个新 dispatchAction 方法,它用来改变这个值。

    这个方法关键的地方在于,这个 dispatchAction 是怎么样改变这个值的。

    dispatchAction 经过如下处理后,只需要接收一个 action 参数:

    (dispatchAction.bind(
        null,
        // Flow doesn't know this is non-null, but we do.
        ((currentlyRenderingFiber: any): Fiber),
        queue,
      ): any)
    
  • update

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

    在组建更新时,调用 updateReducer 方法以达到计算新的状态并触发更新的目的。

useReducer

  • mount

    function mountReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      const hook = mountWorkInProgressHook();
      let initialState;
      if (init !== undefined) {
        initialState = init(initialArg);
      } else {
        initialState = ((initialArg: any): S);
      }
      hook.memoizedState = hook.baseState = initialState;
      const queue = (hook.queue = {
        last: null,
        dispatch: null,
        lastRenderedReducer: reducer,
        lastRenderedState: (initialState: any),
      });
      const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
        null,
        // Flow doesn't know this is non-null, but we do.
        ((currentlyRenderingFiber: any): Fiber),
        queue,
      ): any));
      return [hook.memoizedState, dispatch];
    }
    

    mountReducer 接收三个参数:

    • reducer:状态生成器函数

      reducer 函数的形式为 (state, action) => newState,state 为上一次存储的 state

    • initialArg:初始值

    • init可选参数,初始化函数,它将接收 initialArg 参数作为其唯一的参数执行,执行的结果作为初始状态。

  • update

    function updateReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      const hook = updateWorkInProgressHook();
      const queue = hook.queue;
      invariant(
        queue !== null,
        'Should have a queue. This is likely a bug in React. Please file an issue.',
      );
    
      queue.lastRenderedReducer = reducer;
    
      if (numberOfReRenders > 0) {
        // This is a re-render. Apply the new render phase updates to the previous
        // work-in-progress hook.
        const dispatch: Dispatch<A> = (queue.dispatch: any);
        if (renderPhaseUpdates !== null) {
          // Render phase updates are stored in a map of queue -> linked list
          const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
          if (firstRenderPhaseUpdate !== undefined) {
            renderPhaseUpdates.delete(queue);
            let newState = hook.memoizedState;
            let update = firstRenderPhaseUpdate;
            do {
              // 处理此呈现阶段更新。
              // 我们不需要检查优先级,因为它总是与当前渲染的相同
              // 循环处理更新队列
              const action = update.action;
              newState = reducer(newState, action);
              update = update.next;
            } while (update !== null);
    
            // 标记 fiber 执行的工作,但仅当新状态与当前状态不同时。
            if (!is(newState, hook.memoizedState)) {
              markWorkInProgressReceivedUpdate();
            }
    
            hook.memoizedState = newState;
            // 除非队列是空的,否则不要将渲染阶段更新累积的状态持久化到基本状态。
            // TODO: Not sure if this is the desired semantics, but it's what we
            // do for gDSFP. I can't remember why.
            if (hook.baseUpdate === queue.last) {
              hook.baseState = newState;
            }
    
            queue.lastRenderedState = newState;
    
            return [newState, dispatch];
          }
        }
        return [hook.memoizedState, dispatch];
      }
    
      // 整个队列中的最后一次更新
      const last = queue.last;
      // 最后一次更新,它是 base state 的一部分。
      const baseUpdate = hook.baseUpdate;
      const baseState = hook.baseState;
    
      // 找到第一个未处理的更新。
      let first;
      if (baseUpdate !== null) {
        if (last !== null) {
          // 对于第一次更新,队列是一个循环链表,
          // 其中`queue.last.next = queue.first`.
          // 一旦第一次更新提交,并且 `baseUpdate` 不再为空,我们就可以解开这个列表。
          last.next = null;
        }
        first = baseUpdate.next;
      } else {
        first = last !== null ? last.next : null;
      }
      if (first !== null) {
        let newState = baseState;
        let newBaseState = null;
        let newBaseUpdate = null;
        let prevUpdate = baseUpdate;
        let update = first;
        let didSkip = false;
        do {
          const updateExpirationTime = update.expirationTime;
          if (updateExpirationTime < renderExpirationTime) {
            // Priority is insufficient. Skip this update. If this is the first
            // skipped update, the previous update/state is the new base
            // update/state.
            if (!didSkip) {
              didSkip = true;
              newBaseUpdate = prevUpdate;
              newBaseState = newState;
            }
            // Update the remaining priority in the queue.
            if (updateExpirationTime > remainingExpirationTime) {
              remainingExpirationTime = updateExpirationTime;
            }
          } else {
            // Process this update.
            if (update.eagerReducer === reducer) {
              // If this update was processed eagerly, and its reducer matches the
              // current reducer, we can use the eagerly computed state.
              newState = ((update.eagerState: any): S);
            } else {
              const action = update.action;
              newState = reducer(newState, action);
            }
          }
          prevUpdate = update;
          update = update.next;
        } while (update !== null && update !== first);
    
        if (!didSkip) {
          newBaseUpdate = prevUpdate;
          newBaseState = newState;
        }
    
        // Mark that the fiber performed work, but only if the new state is
        // different from the current state.
        if (!is(newState, hook.memoizedState)) {
          markWorkInProgressReceivedUpdate();
        }
    
        hook.memoizedState = newState;
        hook.baseUpdate = newBaseUpdate;
        hook.baseState = newBaseState;
    
        queue.lastRenderedState = newState;
      }
    
      const dispatch: Dispatch<A> = (queue.dispatch: any);
      return [hook.memoizedState, dispatch];
    }
    

    updateReducer 处理了当前组件的更新队列,它将遍历更新队列,为更新队列中的每一项调用 reducer,上一次计算出的状态会作为参数传递给下一次调用 reducer 的参数。

    最后将计算出的最终的新状态进行缓存,并将其进行返回。

useEffect

  • mount

    function mountEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null,
    ): void {
      return mountEffectImpl(
        UpdateEffect | PassiveEffect,
        UnmountPassive | MountPassive,
        create,
        deps,
      );
    }
    

    mountEffect 的具体实现:mountEffectImpl 方法:

    function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
      const hook = mountWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      sideEffectTag |= fiberEffectTag;
      hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
    }
    

    使用 pushEffect 方法返回一个 Effect 对象,并将其存储到 hook 的缓存中。

    pushEffect 方法:

    创建一个 Effect 对象,添加到 componentUpdateQueue 链表的末尾,并返回这个对象。

    function pushEffect(tag, create, destroy, deps) {
      const effect: Effect = {
        tag,
        create,
        destroy,
        deps,
        // 循环
        next: (null: any),
      };
      if (componentUpdateQueue === null) {
        componentUpdateQueue = createFunctionComponentUpdateQueue();
        componentUpdateQueue.lastEffect = effect.next = effect;
      } else {
        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;
    }
    
  • update

    function updateEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null,
    ): void {
      return updateEffectImpl(
        UpdateEffect | PassiveEffect,
        UnmountPassive | MountPassive,
        create,
        deps,
      );
    }
    

    updateEffect 的具体实现:updateEffectImpl 方法:

    function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
      const hook = updateWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      let destroy = undefined;
    
      if (currentHook !== null) {
        const prevEffect = currentHook.memoizedState;
        destroy = prevEffect.destroy;
        if (nextDeps !== null) {
          const prevDeps = prevEffect.deps;
          // 判断前后的 deps 是否相同
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            // 相同
            // 创建一个新的 Effect,并把它添加到更新队列
            // 但这里我们并不更新 hook 的缓存
            pushEffect(NoHookEffect, create, destroy, nextDeps);
            return;
          }
        }
      }
    
      sideEffectTag |= fiberEffectTag;
      // 创建一个新的 Effect
      hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
    }
    

useLayoutEffect

  • mount

    function mountLayoutEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null,
    ): void {
      return mountEffectImpl(
        UpdateEffect,
        UnmountMutation | MountLayout,
        create,
        deps,
      );
    }
    

    mountLayoutEffect 的实现和 updateEffect 的实现一样。

  • update

    function updateLayoutEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null,
    ): void {
      return updateEffectImpl(
        UpdateEffect,
        UnmountMutation | MountLayout,
        create,
        deps,
      );
    }
    

    updateLayoutEffect 的实现和 updateEffect 的实现一样。

useImperativeHandle

  • mount

    function mountImperativeHandle<T>(
      ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
      create: () => T,
      deps: Array<mixed> | void | null,
    ): void {
      if (__DEV__) {
        // do something
      }
    
      // TODO: If deps are provided, should we skip comparing the ref itself?
      const effectDeps =
        deps !== null && deps !== undefined ? deps.concat([ref]) : null;
    
      return mountEffectImpl(
        UpdateEffect,
        UnmountMutation | MountLayout,
        imperativeHandleEffect.bind(null, create, ref), // 返回一个新 create
        effectDeps,
      );
    }
    
  • update

    function updateImperativeHandle<T>(
      ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
      create: () => T,
      deps: Array<mixed> | void | null,
    ): void {
      if (__DEV__) {
        // do something
      }
    
      // TODO: If deps are provided, should we skip comparing the ref itself?
      const effectDeps =
        deps !== null && deps !== undefined ? deps.concat([ref]) : null;
    
      return updateEffectImpl(
        UpdateEffect,
        UnmountMutation | MountLayout,
        imperativeHandleEffect.bind(null, create, ref), // 返回一个新的 create 函数
        effectDeps,
      );
    }
    

useImperativeHandleuseEffect 在具体实现上都依赖于 mountEffectImplupdateEffectImpl 方法。

只不过 useImperativeHandle 需要多传一个参数——ref,这个参数可以是如下值:

  • {current: T | null}
  • ((inst: T | null) => mixed)
  • null

当存在 deps 的情况下,ref 也会被添加到 deps 后面。

useImperativeHandle 的关键在于 imperativeHandleEffect 方法:

function imperativeHandleEffect<T>(
  create: () => T,
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
) {
  if (typeof ref === 'function') {
    const refCallback = ref;
    const inst = create();
    refCallback(inst);
    return () => {
      refCallback(null);
    };
  } else if (ref !== null && ref !== undefined) {
    const refObject = ref;
    if (__DEV__) {
      // do something
    }
    const inst = create();
    refObject.current = inst;
    return () => {
      refObject.current = null;
    };
  }
}

此方法对原始传入的 createref 做了包装,返回一个新的函数。

useRef

  • mount

    function mountRef<T>(initialValue: T): {current: T} {
      const hook = mountWorkInProgressHook();
      const ref = {current: initialValue};
      if (__DEV__) {
        Object.seal(ref);
      }
      hook.memoizedState = ref;
      return ref;
    }
    
    • mountRef 在组件挂载时获取了一个新的 hook,并将传入的初始值存入了一个对象的 current 属性中,这个对象被存入了 hook 的 memoizedState 属性中。并返回了一个 ref 对象。

    • 为什么 Ref 是一个 { current: any } 的对象呢?仔细一想,如果我们存储的值是一个数字,那么我们想要改变这个值怎么办呢。好像真没办法。但如果是一个对象,那么我们返回的是对这个对象的引用,那么久可以使用 ref.current 对其进行赋值。

      function TextInputWithFocusButton() {
        const inputEl = useRef(null);
        const onButtonClick = () => {
          // `current` 指向已挂载到 DOM 上的文本输入元素
          inputEl.current.focus();
        };
        return (
          <>
            <input ref={inputEl} type="text" />
            <button onClick={onButtonClick}>Focus the input</button>
          </>
        );
      }
      

      在这里,我们先声明了一个 ref,但是其赋值操作却是在 render 阶段完成的。

    • 我们还可以看到,这里对初始值并没有什么要求,因此 Ref 可以任意的值。

    注意:当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。

  • update

    function updateRef<T>(initialValue: T): {current: T} {
      const hook = updateWorkInProgressHook();
      return hook.memoizedState;
    }
    

    这里就简洁明了了,获取了当前这个 hook,并直接读取返回了当前 hook 的缓存值。因此,使用 useRef 返回的 ref 对象在组件的整个生命周期内保持不变。

useCallback

  • mount

    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;
    }
    

    在组建挂载时,该方法创建了一个 hook,这个 hook 将存储传入的 callbackdeps 参数, 最后将 callback 进行了返回。

  • update

    function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
      const hook = updateWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      const prevState = hook.memoizedState;
      if (prevState !== null) {
        if (nextDeps !== null) {
          const prevDeps: Array<mixed> | null = prevState[1];
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            return prevState[0];
          }
        }
      }
      hook.memoizedState = [callback, nextDeps];
      return callback;
    }
    

    在组建更新时,当前后的依赖相同时,始终返回之前 hook 中存储的回调函数;当依赖发生变化时,则返回一个新传入的回调函数。

useMemo

  • mount

    function mountMemo<T>(
      nextCreate: () => T,
      deps: Array<mixed> | void | null,
    ): T {
      const hook = mountWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      const nextValue = nextCreate();
      hook.memoizedState = [nextValue, nextDeps];
      return nextValue;
    }
    

    mountMemo 两个参数,一个 nextCreate 函数和 deps,它创建了一个 hook,并将 nextCreate 函数执行的返回值和 deps 作为数组存入 hook 的缓存中。

    最后返回 nextCreate 函数执行的结果。

  • update

    function updateMemo<T>(
      nextCreate: () => T,
      deps: Array<mixed> | void | null,
    ): T {
      const hook = updateWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      const prevState = hook.memoizedState;
      if (prevState !== null) {
        // 假设这些都已定义。如果没有,areHookInputsEqual 会发出警告。
        if (nextDeps !== null) {
          const prevDeps: Array<mixed> | null = prevState[1];
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            return prevState[0];
          }
        }
      }
      const nextValue = nextCreate();
      hook.memoizedState = [nextValue, nextDeps];
      return nextValue;
    }
    

    与其他有传入 deps 的 hook 一样,useMemo 在组件更新时会检查前后两次传入的 deps 是否按序相等。 如果相等,则返回之前存储的 nextCreate 的执行结果;如果不等,则会重新计算该值,并将最新的结果和新的 deps 进行缓存。最后返回这个新的计算结果。

readContext、useContext

readContextuseContext 同时都是指向同一个方法,并且不区分 mount 和 update:

import {readContext} from './ReactFiberNewContext';

这个方法从其他模块导入,请参见 React 深入学习:ReactFiberNewContext

useDebugValue

  • mount

    function mountDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
      // This hook is normally a no-op.
      // The react-debug-hooks package injects its own implementation
      // so that e.g. DevTools can display custom hook values.
    }
    
    
  • update

    const updateDebugValue = mountDebugValue;
    
    

其他方法

  • mountWorkInProgressHook

    function mountWorkInProgressHook(): Hook {
      const hook: Hook = {
        memoizedState: null,
    
        baseState: null,
        queue: null,
        baseUpdate: null,
    
        next: null,
      };
    
      if (workInProgressHook === null) {
        // 这是列表中的第一个钩子
        firstWorkInProgressHook = workInProgressHook = hook;
      } else {
        // 添加到列表的末尾
        workInProgressHook = workInProgressHook.next = hook;
      }
      return workInProgressHook;
    }
    

    workInProgressHook 也是一个单项的链表结构,mountWorkInProgressHook 该方法用于向 workInProgressHook 链表末尾添加 hook,并返回新增的这个 hook。

  • updateWorkInProgressHook

    function updateWorkInProgressHook(): Hook {
      // This function is used both for updates and for re-renders triggered by a
      // render phase update. It assumes there is either a current hook we can
      // clone, or a work-in-progress hook from a previous render pass that we can
      // use as a base. When we reach the end of the base list, we must switch to
      // the dispatcher used for mounts.
      if (nextWorkInProgressHook !== null) {
        // There's already a work-in-progress. Reuse it.
        workInProgressHook = nextWorkInProgressHook;
        nextWorkInProgressHook = workInProgressHook.next;
    
        currentHook = nextCurrentHook;
        nextCurrentHook = currentHook !== null ? currentHook.next : null;
      } else {
        // Clone from the current hook.
        invariant(
          nextCurrentHook !== null,
          'Rendered more hooks than during the previous render.',
        );
        currentHook = nextCurrentHook;
    
        const newHook: Hook = {
          memoizedState: currentHook.memoizedState,
    
          baseState: currentHook.baseState,
          queue: currentHook.queue,
          baseUpdate: currentHook.baseUpdate,
    
          next: null,
        };
    
        if (workInProgressHook === null) {
          // This is the first hook in the list.
          workInProgressHook = firstWorkInProgressHook = newHook;
        } else {
          // Append to the end of the list.
          workInProgressHook = workInProgressHook.next = newHook;
        }
        nextCurrentHook = currentHook.next;
      }
      return workInProgressHook;
    }
    
  • createFunctionComponentUpdateQueue

    创建函数组件的更新队列

    function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
      return {
        lastEffect: null,
      };
    }
    
  • areHookInputsEqual

    遍历两个数组,判断他们的值是否按序相同,判断方法采用 Object.is

    function areHookInputsEqual(
      nextDeps: Array<mixed>,
      prevDeps: Array<mixed> | null,
    ) {
      if (prevDeps === null) {
        if (__DEV__) {
          // do something
        }
        return false;
      }
    
      if (__DEV__) {
        // do something
      }
      for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
        if (is(nextDeps[i], prevDeps[i])) {
          continue;
        }
        return false;
      }
      return true;
    }
    
  • js

    判断两个值是否相等,它是的 Object.is 函数的 Polyfill

    path:packages/shared/objectIs.js

    function is(x: any, y: any) {
      return (
        (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
      );
    }
    
    

遗留问题

  1. userEffect 的 更新队列何时被调用