本文仅仅是阅读 图解 React 原理系列 的笔记,了解更多内容请查看原文链接。

副作用 Hook

创建 Hook

在 fiber 初次构造阶段,useEffect 对应源码 mountEffect,useLayoutEffect 对应的源码 mountLayoutEffect

function mountEffect(create: () => (() => void) | void, deps: Array<mixed> | void | null): void {
  return mountEffectImpl(PassiveEffect | PassiveStaticEffect, HookPassive, create, deps);
}
function mountLayoutEffect(create: () => (() => void) | void, deps: Array<mixed> | void | null) {
  return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
}

可见 mountEffect 和 mountLayoutEffect 内部都是通过调用 mountEffectImpl。

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps);
}

mountEffectImpl 逻辑:

  1. 创建 hook。
  2. 设置 workInProgress 的副作用标记:flags |= fiberFlags。
  3. 创建 effect,挂载到 hook.memoizedState 上,即 hook.memoizedState = effect

PS:状态 Hook 的 hook.memoizedState = state。

创建 Effect

function pushEffect(tag, create, destroy, deps) {
  // 创建 effect 对象
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    next: (null: any),
  };
  // 将 effect 对象添加到环形链表末尾
  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;
}

effect 的数据结构:

export type Effect = {
  tag: HookFlags,
  create: () => (() => void) | void,
  destroy: (() => void) | void,
  deps: Array<mixed> | null,
  next: Effect,
};
  • effect.tag: HookFlags 类型定义位于 react-reconciler/src/ReactHookEffectTags.js 中,是二进制属性,代表 effect 类型:

    export type HookFlags = number;
    export const NoFlags = /*   */ 0b000;
    export const HasEffect = /* */ 0b001;
    export const Layout = /*    */ 0b010;
    export const Passive = /*   */ 0b100;
    
  • effect.create: 通过 useEffect() 所传入的函数
  • effect.deps:依赖项,如果依赖变动,会创建新的 effect

现在 workInProgress.flags 被打上标记,最后会在 fiber 树渲染阶段的 commitRoot 函数中处理。

区别

从上面可知,useEffect 与 useLayoutEffect 只是内部 flags,tag 状态的区别

  1. fiber.flags 区别:
    • useEffect 的 fiber.flags = UpdateEffect | PassiveEffect
    • useLayoutEffect 的 fiber.flags= UpdateEffect
  2. fiber.tag 的区别:
    • useEffect 的 fiber.tag = HookHasEffect | HookPassive
    • useLayoutEffect 的 fiber.tag = HookHasEffect | HookLayout

处理 Effect 回调

完成 fiber 树构造后,逻辑会进入渲染阶段,在 commitRootImp 函数中,整个渲染过程被 3 个函数分步实现:

  1. commitBeforeMutationEffects
  2. commitMutationEffects
  3. recursivelyCommitLayoutEffects

这 3 个函数会处理 fiber.flags,也会根据情况处理 fiber.updateQueue.lastEffect。

commitMutationEffects

function commitBeforeMutationEffects(firstEffect: Fiber) {
  let fiber = firstEffect;
  while (fiber !== null) {
    if (fiber.deletions !== null) {
      commitBeforeMutationEffectsDeletions(fiber.deletions);
    }

    if (fiber.child !== null) {
      const primarySubtreeFlags = fiber.subtreeFlags & BeforeMutationMask;
      if (primarySubtreeFlags !== NoFlags) {
        commitBeforeMutationEffects(fiber.child);
      }
    }

    try {
      commitBeforeMutationEffectsImpl(fiber);
    } catch (error) {
      captureCommitPhaseError(fiber, fiber.return, error);
    }
    fiber = fiber.sibling;
  }
}

function commitBeforeMutationEffectsImpl(fiber: Fiber) {
  const flags = fiber.flags;

  // 省略其他代码,只保留 Hook 相关

  if ((flags & Passive) !== NoFlags) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }
}

第一阶段:DOM 变更之前,处理副作用队列中带有 Passive 标记的 fiber 节点。

注意:由于 flushPassiveEffects 被包裹在 scheduleCallback 回调中,由调度中心来处理的,且参数是 NormalSchedulerPriority,故这是一个异步回调。

由于 scheduleCallback(NormalSchedulerPriority, callback) 是异步的,flushPassiveEffects 并不会立即执行。

commitMutationEffects

function commitMutationEffects(
  firstChild: Fiber,
  root: FiberRoot,
  renderPriorityLevel: ReactPriorityLevel
) {
  let fiber = firstChild;
  while (fiber !== null) {
    const deletions = fiber.deletions;
    if (deletions !== null) {
      commitMutationEffectsDeletions(deletions, fiber, root, renderPriorityLevel);
    }

    if (fiber.child !== null) {
      const mutationFlags = fiber.subtreeFlags & mutationMask;
      if (mutationFlags !== NoFlags) {
        commitMutationEffects(fiber.child, root, renderPriorityLevel);
      }
    }

    try {
      commitMutationEffectsImpl(fiber, root, renderPriorityLevel);
    } catch (error) {
      captureCommitPhaseError(fiber, fiber.return, error);
    }
    fiber = fiber.sibling;
  }
}

function commitMutationEffectsImpl(fiber: Fiber, root: FiberRoot, renderPriorityLevel) {
  const flags = fiber.flags;

  // 省略其他代码,只保留 Hook 相关

  const primaryFlags = flags & (Placement | Update | Hydrating);

  switch (primaryFlags) {
    case Update: {
      const current = fiber.alternate;
      commitWork(current, fiber);
      break;
    }
  }
}

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  // 省略其他代码,只保留 Hook 相关

  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent:
    case Block: {
      commitWorkEffectListUnmount(HookLayout | HookHasEffect, finishedWork, finishedWork.return);
      return;
    }
  }
}

// 依次执行 effect.destroy
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) {
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

第二阶段:DOM 变更,界面得到更新。

调用关系:commitMutationEffects -> commitWork -> commitHookEffectListUnmount。

  • 注意在调用 commitMutationEffects(HookLayout | HookHasEffect, finishedWork) 时,参数是 HookLayout | HookHasEffect,所以只处理由 useLayoutEffect() 创建的 effect。
  • 根据上文的分析 HookLayout | HookHasEffect 是通过 useLayoutEffect 创建的 effect。所以 commitMutationEffects 函数只能处理由 useLayoutEffect() 创建的 effect。
  • 同步调用 effect.destroy()。

recursivelyCommitLayoutEffects

function recursivelyCommitLayoutEffects(finishedWork: Fiber, finishedRoot: FiberRoot) {
  const { flags, tag } = finishedWork;

  switch (tag) {
    case Profiler: {
      // 省略其他代码
    }

    default: {
      // 省略其他代码
      const primaryFlags = flags & (Update | Callback);
      if (primaryFlags !== NoFlags) {
        switch (tag) {
          case FunctionComponent:
          case ForwardRef:
          case SimpleMemoComponent:
          case Block: {
            commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);

            if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags) {
              schedulePassiveEffectCallback();
            }
            break;
          }
        }
      }
    }
  }
}

function commitHookEffectListMount(flags: HookFlags, 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 & flags) === flags) {
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

更新 Hook

在更新过程中 useEffect 对应的源码 UpdateEffect,useLayoutEffect 对应的源码 updateLayoutEffect。他们内部都是调用 UpdateEffectImpl,与初次创建时一样,只是参数不同。

更新 Effect

function UpdateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  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)) {
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  // 如果依赖变动,更改 fiber.flag,新建 effect
  currentlyRenderingFiber.flags |= fiberFlags;

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

UpdateEffectImpl 与 mountEffectImpl 逻辑有所不同,如果 useEffect/useLayoutEffect 的依赖不变,新建的 effect 对象不带 HasEffect 标记。

注意:无论依赖是否变化,都复用之前的 effect.destroy。等待 commitRoot 阶段的调用。

处理 Effect 回调

新的 Hook 以及新的 Effect 创建完成之后,余下逻辑与初次渲染完全一致。处理 Effect 回调时也会根据 effect.tag 进行判断:只有effect.tag 包含 HookHasEffect 时才会调用 effect.destroy 和 effect.create()

组件销毁

当 function 组件被销毁时,fiber 节点必然会被打上 Deletion 标记,即 fiber.flags = Deletion,带有 Deletion 标记的 fiber 在 commitMutationEffect 被处理

在 commitDeletion 函数之后,继续调用 unmountHostComponents -> commitUnmount,在 commitUnmount 中,执行 effect.destroy(),结束整个闭环。

参考链接