MyPrototypeWhat

React@v19源码---useEffectuse / LayoutEffect

7 Jun 2024

11 min read

useEffect

mountEffect / updateEffect

// PassiveStaticEffect 静态副作用,用于标记组件只在挂载和卸载时运行
mountEffectImpl(PassiveEffect | PassiveStaticEffect, HookPassive, create, deps);

updateEffectImpl(PassiveEffect, HookPassive, create, deps);

除了 mount 的时候多了一个静态标记以外没什么别的区别

mountEffectImpl / updateEffectImpl

function mountEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  // 创建hook对象在上文已经讲过
  const hook = mountWorkInProgressHook();
  // useEffect的第二个参数
  const nextDeps = deps === undefined ? null : deps;
  // 给当前组件的fiber打上副作用标记
  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    // 创建一个对象 {destroy:undefined}
    createEffectInstance(),
    nextDeps
  );
}

function updateEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const effect: Effect = hook.memoizedState;
  const inst = effect.inst;
  // 在渲染阶段state更新后或严格模式下重新渲染时,初始加载时 currentHook 为空
  if (currentHook !== null) {
    if (nextDeps !== null) {
      const prevEffect: Effect = currentHook.memoizedState;
      const prevDeps = prevEffect.deps;
      // 判断前后两个deps是否相等
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
        return;
      }
    }
  }
  // 依赖发生变化时才会打上副作用标记
  currentlyRenderingFiber.flags |= fiberFlags;
  // HookHasEffect 代表需要触发effect回调
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    inst,
    nextDeps
  );
}

总结

其实mount和 update 执行的函数没什么太大区别。pushEffect才是核心

  • 创建/获取 hook 对象,
  • 处理deps
  • 执行pushEffect

hook.memoizedState里保存的是 effect 对象

需要注意的点:

  • PassiveEffect、PassiveStaticEffect 是 fiber.flags 上的标记,用来判断是否有副作用相关的逻辑,跟HookPassive、HookHasEffect不一样,HookPassive是 hooks 上的tag标记,别搞混了
  • 依赖对比是通过Object.is进行比较的,跟 props 和 state 是否更改的判断方法一样
  • inst 是个对象,有个destory函数,这个就是 useEffect 第一个参数回调函数的返回值

pushEffect

function pushEffect(
  tag: HookFlags,
  create: () => (() => void) | void,
  inst: EffectInstance,
  deps: Array<mixed> | null
): Effect {
  const effect: Effect = {
    tag,
    // create 为 useEffect第一个参数
    create,
    // inst
    inst,
    // deps 为 useEffect第二个参数
    deps,
    // Circular
    next: (null: any),
  };
  let componentUpdateQueue: null | FunctionComponentUpdateQueue =
    (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    // 函数式组件的updateQueue结构为
    // {
    //   lastEffect: null,
    //   events: null,
    //   stores: 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;
      // 将最新的effect串在表尾
      lastEffect.next = effect;
      // 闭环
      effect.next = firstEffect;
      // 更新表尾
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}
总结

pushEffect函数主要做了两件事情:

  • 创建 effect
  • 将 effect 串成环状链表,与 fiber.updateQueue.lastEffect 关联

fiber.updateQueue 保存的是 updateQueue 对象,其中lastEffect保存的是 effect 对象,是环状链表的表尾,effect 对象通过 next 连接当前函数组件的所有 effect组成环状链表,当前 useEffect语句 对应的 hook 对象中memoizedState保存的是 effect 对象,在 updateEffectImpl 中会获取,比较前后两个hook的依赖是否更改

useEffect 回调什么时候触发呢?

给 fiber.flags 合并上副作用标记之后,会在 compeleteWork 阶段将 flags 向上冒泡到 HostRootFiber.subtreeFlags,在 commit 阶段的commitRootImpl函数内,通过

if (
  // 判断子节点是否包含PassiveMask
  (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
  // 判断当前节点是否包含PassiveMask
  (finishedWork.flags & PassiveMask) !== NoFlags
) {
  // 是否已经有副作用被调度
  if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    pendingPassiveEffectsRemainingLanes = remainingLanes;

    pendingPassiveTransitions = transitions;
    // 调度flushPassiveEffects
    scheduleCallback(NormalSchedulerPriority, function () {
      // 这个函数是核心
      flushPassiveEffects();
      return null;
    });
  }
}
// ...
// commitBeforeMutationEffects
// commitMutationEffects
// commitLayoutEffects
// ...
if (rootDoesHavePassiveEffects) {
  // 有有副作用,赋值这些变量
  rootDoesHavePassiveEffects = false;
  rootWithPendingPassiveEffects = root;
  pendingPassiveEffectsLanes = lanes;
}
// ...
// 如果当前渲染是离散渲染(click,touch等事件),则直接执行flushPassiveEffects
if (
  includesSyncLane(pendingPassiveEffectsLanes) &&
  (disableLegacyMode || root.tag !== LegacyRoot)
) {
  flushPassiveEffects();
}
// ...

因为只是个调度,是个异步,并不会马上触发,所以触发时机在所有渲染流程结束之后触发

  • 触发时机一定是在 commit 三大阶段之后触发的,也就是真实 dom 渲染到页面之后触发
  • 是不是异步?因为整个渲染流程是 MessageChannel 的 postMessage 触发的,所以整个渲染流程都是异步,flushPassiveEffects 在异步任务中执行,自然也是异步,但是相对于渲染流程来说不一定是异步,有两点原因:
    • 通过 scheduleCallback 调度的flushPassiveEffects,scheduleCallback的逻辑是如果当前的任务不超过 5ms,则继续同步执行下一个任务,也就是说如果整个渲染流程不超过 5ms,那么会同步flushPassiveEffects
      • 如果超过了 5ms,则通过MessageChannel的 postMessage 触发flushPassiveEffects,就会被放进下一个事件循环中执行
    • 如果当前的 lane 是同步的,则同步执行flushPassiveEffects

flushPassiveEffects

export function flushPassiveEffects(): boolean {
  if (rootWithPendingPassiveEffects !== null) {
    const root = rootWithPendingPassiveEffects;
    const remainingLanes = pendingPassiveEffectsRemainingLanes;
    pendingPassiveEffectsRemainingLanes = NoLanes;
    // 转换成事件优先级
    const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
    // 找到最低事件优先级
    const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
    const prevTransition = ReactSharedInternals.T;
    const previousPriority = getCurrentUpdatePriority();

    try {
      // 设置更新优先级,原生事件触发时也是调用的这个
      // 设置ReactDOMSharedInternals.p
      setCurrentUpdatePriority(priority);
      ReactSharedInternals.T = null;
      return flushPassiveEffectsImpl();
    } finally {
      setCurrentUpdatePriority(previousPriority);
      ReactSharedInternals.T = prevTransition;
      releaseRootPooledCache(root, remainingLanes);
    }
  }
  return false;
}

flushPassiveEffectsImpl

function flushPassiveEffectsImpl() {
  if (rootWithPendingPassiveEffects === null) {
    return false;
  }

  const transitions = pendingPassiveTransitions;
  pendingPassiveTransitions = null;
  // 保存有副作用的fiber的fiberRootNode和待处理的副作用优先级,然后清空全局变量
  const root = rootWithPendingPassiveEffects;
  const lanes = pendingPassiveEffectsLanes;
  rootWithPendingPassiveEffects = null;

  pendingPassiveEffectsLanes = NoLanes;

  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    throw new Error("Cannot flush passive effects while already rendering.");
  }

  // 保存当前执行上下文,处理完之后恢复
  const prevExecutionContext = executionContext;
  // 进入commit阶段
  // 这个操作除了这个地方之外 还有在commit阶段(commitRootImpl)
  executionContext |= CommitContext;
  // 先执行unMount 后执行mount
  // 两个操作逻辑基本一致 从root开始递归检查subtreeFlags是否有对应flag,
  // 一直继续向下遍历,到最底
  // 深度优先的后续遍历,跟commit阶段一个逻辑
  commitPassiveUnmountEffects(root.current);
  commitPassiveMountEffects(root, root.current, lanes, transitions);

  // 恢复处理effect之前的执行上下文
  executionContext = prevExecutionContext;
  // 所有异步结束之前都要刷新一下同步任务
  flushSyncWorkOnAllRoots();

  return true;
}

commitHookEffectListUnmount

unmount 先触发

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) {
    // 遍历所有effect
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      //
      if ((effect.tag & flags) === flags) {
        // 是否包含flags
        // Unmount
        const inst = effect.inst;
        const destroy = inst.destroy;
        if (destroy !== undefined) {
          // 清空destroy,以便重新生成
          inst.destroy = undefined;
          // 执行destroy
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

commitPassiveMountEffects

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) {
        // Mount
        const create = effect.create;
        const inst = effect.inst;
        // 执行useEffect第一个参数
        const destroy = create();
        // 更新destroy
        inst.destroy = destroy;
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
总结

可以看到总体的 useEffect 执行流程,destroy先执行(第一次没有,因为还没有执行回调哪来的返回值),顺序是由子->父,然后再执行回调,顺序是子->父

useLayoutEffect

mountEffectImpl / updateEffectImpl

mountEffectImpl(UpdateEffect | LayoutStaticEffect, HookLayout, create, deps);

updateEffectImpl(UpdateEffect, HookLayout, create, deps);

可以看到跟 useEffect 调用的是同一个函数,但是 fiberFlags 传的是 Update,effect 的 tag 是 HookLayout

触发时机在哪呢

在 commit 阶段的 layout 阶段

// commitRoot中设置优先级为离散事件优先级,也就是SyncLane
setCurrentUpdatePriority(DiscreteEventPriority);
// ⬇️ `layout` 阶段
commitLayoutEffects(finishedWork, root, lanes);
// ⬇️
commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
// ⬇️
if (flags & Update) {
  // 同步去执行useLayoutEffect的回调
  // 这里传入的是HookLayout | HookHasEffect 刚好跟useLayoutEffect的effect.tag对应
  commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
}
// ⬇️
// 上文讲过就不重复了
commitHookEffectListMount(hookFlags, finishedWork);
总结

可以看到回调是在 commitLayoutEffects 阶段执行的,而且通过在 commitRoot 函数内设置了同步 lane。 如果回调内调用 setState,那么调度出的任务也是个同步任务,在 commit 三大阶段完成后会刷新同步任务,所以可以看到:如果在回调内更改 state,页面并不会闪烁 useEffect 打上的 fiber.flags 是 passive,useLayoutEffect 打上的是 update