React 源码阅读 - Hooks2

317 阅读9分钟

接上一篇,这一篇再说一下另外几个常用的 hookuseEffectuseLayoutuseRefuseMemouseCallbackuseImperativeHandle

useEffect 与 useLayoutEffect

useEffectuseLayoutEffect 除了在执行时机上不一样其它基本上都一样。本文将以 useEffect 为主去介绍两个 API

useEffectuseLayoutEffect 的生成也分为 mountupdate 阶段。

mount

mount 阶段主要是进行副作用的挂载,关键的函数调用是 mountEffect(mountLayoutEffect) -> mountEffectImpl -> pushEffectpushEffect 主要作用就是把副作用挂到 fiberupdateQueue 属性上,与 useStateupdate 对象一致,也是一个环状链表结构。

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  // 副作用挂到 memoizedState 的时候也被挂到了 updateQueue 上
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    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;
}

生成的副作用的数据结构如下图所示:

hooks3.png

update

update 阶段和 mount 阶段做的时候大致相同,但是增加了对依赖项的判断。其关键调用的函数 updateEffect(updateLayoutEffect) -> updateEffectImpl -> pushEffect

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function updateEffectImpl(fiberFlags, hookFlags, 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;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

上面这段代码中 updateEffectImpl 函数里面会判断依赖项(deps) 其后是否有变化,调用了 areHookInputsEqual 函数,这个函数内部实际上是用了 Object.is 这个 API 来判断的。

调用过程

useEffectuseLayoutEffect 的调用过程有些不同,在 React 源码阅读 - 渲染 中提到了,useEffectcommit 阶段的 before mutation 阶段进入之前会调用 flushPassiveEffects 函数。而 useLayoutEffect 则是在 commit 阶段的 mutationlayout 阶段调用的。

不过最后会经过调度器的处理,所以 useEffect 最终表现出来执行时机比 uesLayoutEffect 要晚。

useEffect

flushPassiveEffects 内部基本都是一些设置优先级的操作,然后调用了 flushPassiveEffectsImpl 函数,从而触发 useEffect 的调用。

// path: packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function flushPassiveEffectsImpl() {
  // 省略一些代码
  // 调用上一次 useEffect 的销毁函数
  commitPassiveUnmountEffects(root.current);
  // 调用本次 useEffect 的回调函数
  commitPassiveMountEffects(root, root.current);

  // 省略一些代码

  flushSyncCallbacks();

  // 省略一些代码

  return true;
}

销毁函数执行

销毁函数也就是我们在 useEffect 的回调中写的 return 的那部分函数。

以官网的一段代码为例:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
});

ChatAPI.unsubscribeFromFriendStatus 就是销毁函数部分。return 以外的就是回调函数部分。

commitPassiveUnmountEffects 函数正是执行销毁函数的地方,它内部又调用了 commitPassiveUnmountEffects_begin -> commitPassiveUnmountEffects_complete -> commitPassiveUnmountOnFiber -> commitHookEffectListUnmount

// path: packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitHookEffectListUnmount(
  flags: HookFlags,
  finishedWork: Fiber,
  nearestMountedAncestor: Fiber | null,
) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

回调函数执行

commitPassiveMountEffects 函数正是执行回调函数的地方,它内部又调用了 commitPassiveMountEffects_begin -> commitPassiveMountEffects_complete -> commitPassiveMountOnFiber -> commitHookEffectListMount。这个过程和销毁阶段执行的过程一样。

// path: packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & tag) === tag) {
        // Mount
        const create = effect.create;
        effect.destroy = create();

        if (__DEV__) {
          const destroy = effect.destroy;
          if (destroy !== undefined && typeof destroy !== 'function') {
            let addendum;
            if (destroy === null) {
              addendum =
                ' You returned null. If your effect does not require clean ' +
                'up, return undefined (or nothing).';
            } else if (typeof destroy.then === 'function') {
              addendum =
                '\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' +
                'Instead, write the async function inside your effect ' +
                'and call it immediately:\n\n' +
                'useEffect(() => {\n' +
                '  async function fetchData() {\n' +
                '    // You can await here\n' +
                '    const response = await MyAPI.getData(someId);\n' +
                '    // ...\n' +
                '  }\n' +
                '  fetchData();\n' +
                `}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
                'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching';
            } else {
              addendum = ' You returned: ' + destroy;
            }
            console.error(
              'An effect function must not return anything besides a function, ' +
                'which is used for clean-up.%s',
              addendum,
            );
          }
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

useLayoutEffect

销毁函数执行

useLayoutEffect 销毁函数与 useEffect 的销毁函数一样,也是回调中写的 return 的那部分函数。

commit 阶段的 mutation 阶段,删除组件的时候会调用 commitDeletion -> commitNestedUnmounts -> commitUnmount,

// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitUnmount(
  finishedRoot: FiberRoot,
  current: Fiber,
  nearestMountedAncestor: Fiber,
): void {
  onCommitUnmount(current);

  switch (current.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
      if (updateQueue !== null) {
        const lastEffect = updateQueue.lastEffect;
        if (lastEffect !== null) {
          const firstEffect = lastEffect.next;

          let effect = firstEffect;
          do {
            const {destroy, tag} = effect;
            if (destroy !== undefined) {
              if ((tag & HookLayout) !== NoHookEffect) {
                if (
                  enableProfilerTimer &&
                  enableProfilerCommitHooks &&
                  current.mode & ProfileMode
                ) {
                  startLayoutEffectTimer();
                  safelyCallDestroy(current, nearestMountedAncestor, destroy);
                  recordLayoutEffectDuration(current);
                } else {
                  safelyCallDestroy(current, nearestMountedAncestor, destroy);
                }
              }
            }
            effect = effect.next;
          } while (effect !== firstEffect);
        }
      }
      return;
    }
    // 省略一些代码
  }
}

回调函数执行

useLayoutEffect 的回调函数调用是在 commit 阶段的 layout 阶段进行调用的。

// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        if (
          !enableSuspenseLayoutEffectSemantics ||
          !offscreenSubtreeWasHidden
        ) {
          if (
            enableProfilerTimer &&
            enableProfilerCommitHooks &&
            finishedWork.mode & ProfileMode
          ) {
            try {
              startLayoutEffectTimer();
              // 执行 useLayoutEffect 的回调函数
              commitHookEffectListMount(
                HookLayout | HookHasEffect,
                finishedWork,
              );
            } finally {
              recordLayoutEffectDuration(finishedWork);
            }
          } else {
            commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
          }
        }
        break;
      }
      // 省略一些代码
    }
  }
  // 省略一些代码
}

可以看到 useLayoutEffectuseEffect 回调的时候都一样调用了 commitHookEffectListMount 函数。

useMemo 与 useCallback

这两个 hook 比较类似,所以放一起看,这两个 hook 也分为两种情况:mountupdate

mount

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 创建并返回当前 hook
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 计算要返回的值
  const nextValue = nextCreate();
  // 将计算好的值和依赖数组保存在 hook.memoizedState
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 创建并返回当前 hook
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 将回调函数和依赖数组保存在 hook.memoizedState
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

可以看到,这两个挂载函数唯一的区别是:

  • mountMemo 会将回调函数(nextCreate)的执行结果作为 value 保存
  • mountCallback 会保存回调函数

update

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 返回当前 hook
  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];
      }
    }
  }
  // 变化之后重新计算值
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 返回当前 hook
  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;
}

可以看到,这两个更新函数的区别也是在于返回的是计算好的值还是回调函数。

useRef

useRef 也分为 mountupdate 两个场景。

mount

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function mountRef<T>(initialValue: T): {|current: T|} {
  // 获取当前的 hook
  const hook = mountWorkInProgressHook();
  if (enableUseRefAccessWarning) { // false
    if (__DEV__) {
      // 省略一些代码
    } else {
      const ref = {current: initialValue};
      hook.memoizedState = ref;
      return ref;
    }
  } else {
    // 创建 hook
    const ref = {current: initialValue};
    // 将 ref 保存在 memoizedState 属性上
    hook.memoizedState = ref;
    // 返回 ref
    return ref;
  }
}

可见,useRef 仅仅是返回一个包含 current 属性的对象。

React.createRef 做的事情和上面代码做的事情一致:

// path: packages/react/src/ReactCreateRef.js
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  // 省略一些代码
  return refObject;
}

update

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
function updateRef<T>(initialValue: T): {|current: T|} {
  // 获取当前 hook
  const hook = updateWorkInProgressHook();
  // 返回之前存储的值
  return hook.memoizedState;
}

可见 useRef 如果只是用来存一些值,那么只要不主动去更新他,那么在函数组件更新的时候 useRef 是不会变的。

ref 上挂 DOM 对象的流程

React 中,HostComponentClassComponentForwardRef 可以赋值 ref 属性,如下所示。

// HostComponent
<div ref={domRef}></div>

// ClassComponent / Forward
<App ref={componentRef} />

ForwardRef 只是将 ref 作为第二参数传递下去:

const FancyInput = forwardRef((props, ref) => { // props 就算没有使用也不能省略,否则会报错
  const inputRef = useRef()
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    }
  }))
  return (
    <div>
      <input ref={inputRef} type='text' />
      <button onClick={() => inputRef.current.focus()}>click me!</button>
    </div>
  )
})

ForwardRef 组件第二参数 ref 会被传递下去,不会进入 DOM 对象挂上 ref 的流程,所以下面不讨论 Forwardref

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
// renderWithHooks 函数
let children = Component(props, secondArg);

render 阶段

首先是为含有 reffiber 添加 Ref flags

HostComponent: beginWork -> updateHostComponent -> markRef

ClassComponent: beginWork -> updateClassComponent -> finishClassComponent -> markRef

// path: packages/react-reconciler/src/ReactFiberBeginWork.new.js
function markRef(current: Fiber | null, workInProgress: Fiber) {
  const ref = workInProgress.ref;
  if (
    // mount 时 存在 ref 属性
    (current === null && ref !== null) ||
    // update 时 ref 属性改变
    (current !== null && current.ref !== ref)
  ) {
    // Schedule a Ref effect
    workInProgress.flags |= Ref;
    if (enableSuspenseLayoutEffectSemantics) {
      workInProgress.flags |= RefStatic;
    }
  }
}

completeWork 阶段其实也有对 markRef 的调用(不是同一个 markRef),总体来说实现是一致的,不在此赘述了。

总结起来作用就是给组件对应 fiber 赋值 Ref flag,打上标签,以供后续阶使用。

commit 阶段

commit 阶段的 mutation 阶段,会对 ref 进行更改,具体调用如下:

// path: packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
  const flags = finishedWork.flags;

  if (flags & ContentReset) {
    commitResetTextContent(finishedWork);
  }

  if (flags & Ref) {
    const current = finishedWork.alternate;
    if (current !== null) {
      // 移除之前的 ref
      commitDetachRef(current);
    }
    // 省略一些代码
  }
  // 省略一些代码
}

function commitDetachRef(current: Fiber) {
  const currentRef = current.ref;
  if (currentRef !== null) {
    if (typeof currentRef === 'function') {
      if (
        enableProfilerTimer &&
        enableProfilerCommitHooks &&
        current.mode & ProfileMode
      ) {
        try {
          startLayoutEffectTimer();
          // function 类型的 ref 会被调用,入参 null
          currentRef(null);
        } finally {
          recordLayoutEffectDuration(current);
        }
      } else {
        currentRef(null);
      }
    } else {
      // 对象类型的 ref,current 赋值为 null
      currentRef.current = null;
    }
  }
}

可以看出来上面这个过程是对原有 ref 进行了一个删除操作,接下来会进入 ref 的赋值阶段,这个过程是在 commit 阶段的 layout 阶段进行的。

// path: packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  // 省略一些代码
  if (enableScopeAPI) {
    // TODO: This is a temporary solution that allowed us to transition away
    // from React Flare on www.
    if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
      commitAttachRef(finishedWork);
    }
  } else {
    if (finishedWork.flags & Ref) {
      commitAttachRef(finishedWork);
    }
  }
}

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    // stateNode 存的就是 fiber 对应的 DOM 信息
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
    if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
      instanceToUse = instance;
    }
    if (typeof ref === 'function') {
      if (
        enableProfilerTimer &&
        enableProfilerCommitHooks &&
        finishedWork.mode & ProfileMode
      ) {
        try {
          startLayoutEffectTimer();
          // ref 赋值
          ref(instanceToUse);
        } finally {
          recordLayoutEffectDuration(finishedWork);
        }
      } else {
        ref(instanceToUse);
      }
    } else {
      // 省略一些代码

      ref.current = instanceToUse;
    }
  }
}

至此,ref 挂载完毕。

useImperativeHandle

useImperativeHandlerefFroward 配合使用,一般用于暴露某个组件的 API 供其他组件进行调用。

useImperativeHandle 的实现比较简单,直接上代码:

// path: packages/react-reconciler/src/ReactFiberHooks.new.js
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;
    // 省略一些代码
    const inst = create();
    refObject.current = inst;
    return () => {
      refObject.current = null;
    };
  }
}
        
function mountImperativeHandle<T>(
  ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  // 省略一些代码

  const effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;

  let fiberFlags: Flags = UpdateEffect;
  if (enableSuspenseLayoutEffectSemantics) {
    fiberFlags |= LayoutStaticEffect;
  }
  // 省略一些代码
  return mountEffectImpl(
    fiberFlags,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

function updateImperativeHandle<T>(
  ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  // 省略一些代码

  const effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;

  return updateEffectImpl(
    UpdateEffect,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

可以看出 useImperativeHandle 就是调了一下 useEffect 的实现 mountEffectImpl,只是第三参数有一些区别。第三参数使用了 imperativeHandleEffect 函数。

imperativeHandleEffect 函数做的事情就是:

  1. 执行 create 函数,得到实例;
  2. 把实例挂到 ref 的 current 上;