React Fiber源码笔记(八):冲刷副作用

159 阅读6分钟

前言:最近离职准备面试,把之前写的笔记整理一下发出来,本人能力有限,如有错误的地方尽情指正
博客链接:pionpill

对应源码: github.com/facebook/re…

react18 中,副作用是指 useState 钩子导致的副作用,可以被分为以下几种:

  • 卸载副作用(passive unmount effects):在组件被卸载时执行的副作用,比如取消事件订阅、释放资源等。
  • 挂载副作用(passive mount effects): 在组件被挂载时执行的副作用,比如订阅事件、初始化资源等。
  • 更新副作用(passive update effects): 在组件更新时执行的副作用,但不会触发组件重新渲染。

我们要知道 react 副作用的执行是异步的,看下面这段代码:

scheduleCallback(NormalSchedulerPriority, () => {
  flushPassiveEffects();
  return null;
});

Schedular 将 flushPassiveEffects 的执行放在了 NormalSchedulerPriority 中,这是一个低优先级,让浏览器有空了就执行一下副作用处理函数。但很多情况下我们的方法走到某处需要保证副作用已经执行完了,这时候就会在代码中强制执行 flushPassiveEffects

这个方法非常非常的重要,直接关系到我们写 useEffect 等副作用钩子(✨约3174行):

export function flushPassiveEffects(): boolean {
  // rootWithPendingPassiveEffects 用于跟踪具有待处理效应的根节点,这个变量的赋值会在后续操作中进行
  if (rootWithPendingPassiveEffects !== null) {
    const root = rootWithPendingPassiveEffects;

    // 优先级处理,因为这个方法可能从很多地方调用(暂时不管)
    const remainingLanes = pendingPassiveEffectsRemainingLanes;
    pendingPassiveEffectsRemainingLanes = NoLanes;
    const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
    const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
    const prevTransition = ReactCurrentBatchConfig.transition;
    const previousPriority = getCurrentUpdatePriority();

    try {
      ReactCurrentBatchConfig.transition = null;
      setCurrentUpdatePriority(priority);
      // 核心方法
      return flushPassiveEffectsImpl();
    } finally {
      setCurrentUpdatePriority(previousPriority);
      ReactCurrentBatchConfig.transition = prevTransition;
      releaseRootPooledCache(root, remainingLanes);
    }
  }
  return false;
}

核心方法在 flushPassiveEffectsImpl 里面,其他都是异步优先级处理(✨约3226行):

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

  const transitions = pendingPassiveTransitions;
  pendingPassiveTransitions = null;
  const root = rootWithPendingPassiveEffects;
  const lanes = pendingPassiveEffectsLanes;
  rootWithPendingPassiveEffects = null;

  // 卸载副作用与钩子
  commitPassiveUnmountEffects(root.current);
  commitPassiveMountEffects(root, root.current, lanes, transitions);
  flushSyncWorkOnAllRoots();
  return true;
}

commitPassiveUnmountOnFiber

在原生 DOM 中,如果我们仅删除了 DOM 结构,却没有手动释放一些点击,悬浮等事件的回调函数,这些方法仍然会存在于内存中。因此,react 必须保证删除 DOM 的同时将相关的卸载副作用删除

commitPassiveUnmountEffects 方法用于卸载副作用,最终是调用了 commitPassiveUnmountOnFiber 方法(✨约4240行):

function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      recursivelyTraversePassiveUnmountEffects(finishedWork);
      if (finishedWork.flags & Passive) {
        commitHookPassiveUnmountEffects(
          finishedWork,
          finishedWork.return,
          HookPassive | HookHasEffect,
        );
      }
      break;
    }
    case OffscreenComponent: {
      // 这是一个开发中的 API,我们暂时不管这里的逻辑
    }
    default: {
      recursivelyTraversePassiveUnmountEffects(finishedWork);
      break;
    }
  }
}

recursivelyTraversePassiveUnmountEffects

所有类型的节点都会调用 recursivelyTraversePassiveUnmountEffects 方法,该方法遍历要删除的节点及其子节点,卸载无用的副作用(✨约4207行)。

function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
  // 获取要删除的子节点
  const deletions = parentFiber.deletions;

  if ((parentFiber.flags & ChildDeletion) !== NoFlags && deletions !== null) {
    for (let i = 0; i < deletions.length; i++) {
      const childToDelete = deletions[i];
      nextEffect = childToDelete;
      // 删除卸载副作用
      commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
        childToDelete,
        parentFiber,
      );
    }
    detachAlternateSiblings(parentFiber);
  }

  if (parentFiber.subtreeFlags & PassiveMask) {
    let child = parentFiber.child;
    while (child !== null) {
      // 处理子节点
      commitPassiveUnmountOnFiber(child);
      child = child.sibling;
    }
  }
}

commitHookEffectListUnmount

另外还有一个 commitHookPassiveUnmountEffects 方法,这个方法只会在节点类型为 FunctionComponent, ForwardRef, SimpleMemoComponent 时调用。commitHookPassiveUnmountEffects 方法最终会调 commitHookEffectListUnmount 方法卸载一些钩子(✨约567行):

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 inst = effect.inst;
        const destroy = inst.destroy; // 卸载副作用的方法
        if (destroy !== undefined) {
          inst.destroy = undefined;
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

commitPassiveMountOnFiber

commitPassiveMountOnFiber 方法用于执行一些副作用,最终是调用了 commitPassiveMountOnFiber 方法,这个方法逻辑上和 commitPassiveUnmountOnFiber 类似,不同 tag 类型的 FiberNode 处理过程略有不同(✨约3563行):

function commitPassiveMountOnFiber(
  finishedRoot: FiberRoot,
  finishedWork: Fiber,
  committedLanes: Lanes,
  committedTransitions: Array<Transition> | null,
): void {
  const flags = finishedWork.flags;
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      recursivelyTraversePassiveMountEffects(
        finishedRoot,
        finishedWork,
        committedLanes,
        committedTransitions,
      );
      if (flags & Passive) {
        commitHookPassiveMountEffects(
          finishedWork,
          HookPassive | HookHasEffect,
        );
      }
      break;
    }
    case xxx: {
      // 省略不同 tag 处理过程
    }
    default: {
      recursivelyTraversePassiveMountEffects(
        finishedRoot,
        finishedWork,
        committedLanes,
        committedTransitions,
      );
      break;
    }
  }
}

recursivelyTraversePassiveMountEffects

recursivelyTraversePassiveMountEffects 这个方法会遍历子节点并调用 commitPassiveMountOnFiber 方法(✨约3540行):

function recursivelyTraversePassiveMountEffects(
  root: FiberRoot,
  parentFiber: Fiber,
  committedLanes: Lanes,
  committedTransitions: Array<Transition> | null,
) {
  if (parentFiber.subtreeFlags & PassiveMask) {
    let child = parentFiber.child;
    while (child !== null) {
      commitPassiveMountOnFiber(
        root,
        child,
        committedLanes,
        committedTransitions,
      );
      child = child.sibling;
    }
  }
}

commitHookEffectListMount

然后我们看一下 commitHookPassiveMountEffects 方法,这个方法会执行具体的副作用(✨约3363行):

function commitHookPassiveMountEffects(
  finishedWork: Fiber,
  hookFlags: HookFlags,
) {
  try {
    commitHookEffectListMount(hookFlags, finishedWork);
  } catch (error) {
    captureCommitPhaseError(finishedWork, finishedWork.return, error);
  }
}
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;
        const inst = effect.inst;
        // 执行副作用
        const destroy = create();
        // 返回 destroy,方便卸载时清理
        inst.destroy = destroy;
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

commitHookEffectListMount 方法中,我们执行了副作用并返回一个 destroy,方便组件卸载时卸载这些副作用。

这里有一个非常非常非常关键的东西: updateQueue(这里简单讲一下,后面会单独讲他),它是我们函数组件的状态更新队列,先看一下它的数据结构(✨约240行):

export type FunctionComponentUpdateQueue = {
  // 注意想最后一个 Effect 的指针
  lastEffect: Effect | null,
  // 存储组件相关的事件处理函数的 payload
  events: Array<EventFunctionPayload<any, any, any>> | null,
  // 存储组件相关的状态管理库一致性检查函数
  stores: Array<StoreConsistencyCheck<any>> | null,
  // memo 的缓存值
  memoCache?: MemoCache | null,
};

这里面比较重要的就是 Effect 对象(✨约214行):

export type Effect = {
  // 一个标记,表示 effect 的类型
  tag: HookFlags,
  // 用于创建 Effect 函数,返回一个 destroy 清理函数
  create: () => (() => void) | void,
  // 实例,用来存储额外数据的,目前仅会存储 destroy 函数
  inst: EffectInstance,
  // effect 依赖项,依赖发生关系时,effect 重新执行
  deps: Array<mixed> | null,
  // 指向下一个 effect
  next: Effect,
};

简单来说 commitHookEffectListMount 方法会遍历所有的 effect 并执行,并且将执行后返回的 destroy 方法挂载到 inst 属性上,方便以后卸载。

这里简单看一下 updateQueue,后面讲钩子的时候会着重分析他。

flushSyncWorkOnAllRoots

flushSyncWorkOnAllRoots 方法会在最后执行,它用于在所有的根节点上同步执行未完成的工作(✨约142行):

export function flushSyncWorkOnAllRoots() {
  flushSyncWorkAcrossRoots_impl(false);
}
function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
  if (isFlushingWork || !mightHavePendingSyncWork) {
    return;
  }

  let didPerformSomeWork;
  let errors: Array<mixed> | null = null;
  isFlushingWork = true;
  do {
    didPerformSomeWork = false;
    let root = firstScheduledRoot;
    while (root !== null) {
      if (onlyLegacy && root.tag !== LegacyRoot) {
        // Skip non-legacy roots.
      } else {
        const workInProgressRoot = getWorkInProgressRoot();
        const workInProgressRootRenderLanes =
          getWorkInProgressRootRenderLanes();
        const nextLanes = getNextLanes(
          root,
          root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
        );
        if (includesSyncLane(nextLanes)) {
          // 存在同步任务
          try {
            didPerformSomeWork = true;
            // 执行同步任务逻辑
            performSyncWorkOnRoot(root, nextLanes);
          } catch (error) {
            errors === null ? errors = [error] : errors.push(error);
          }
        }
      }
      root = root.next;
    }
  } while (didPerformSomeWork);
  isFlushingWork = false;

  // 错误处理
  if (errors !== null) {
    if (errors.length > 1) {
      if (typeof AggregateError === 'function') {
        // eslint-disable-next-line no-undef
        throw new AggregateError(errors);
      } else {
        for (let i = 1; i < errors.length; i++) {
          scheduleImmediateTask(throwError.bind(null, errors[i]));
        }
        const firstError = errors[0];
        throw firstError;
      }
    } else {
      const error = errors[0];
      throw error;
    }
  }
}

这个方法核心逻辑是检测到有同步任务要执行的时候,执行同步任务,很好理解。副作用怎么会产生同步任务呢?比如 useLayoutEffect 会在 DOM 更新前同步执行,因此产生了同步任务。

好,最后整理一下 flushPassiveEffects 的逻辑:

flushPassiveEffects