react学习笔记-3 - Shiina's Blog

react学习笔记-3

2024 年 7 月 1 日 - 00:21:41 发布
8.7K 字 29分钟
本作品采用 CC BY-NC-SA 4.0 进行许可。
本文距离首次发布已经过去了 88 天,请注意文章的时效性!

react学习笔记-3

此处开始讲 commit 流程

commit 的三个过程:

  • before mutation阶段(执行DOM操作前)
  • mutation阶段(执行DOM操作)
  • layout阶段(执行DOM操作后)

一个一个来吧

before mutation

在 react 18 中的 before mutation effect 的方法执行之前有(好长一段)[https://github.com/facebook/react/tree/main/packages/react-reconciler/src/ReactFiberWorkLoop.js#L2798]

具体作用如下(我乱写的,别看:

  • 调度 useEffect 至异步宏任务
  • 初始化根节点
  • 处理渲染管线
  • 重置 render 目标

假如检查根节点有副作用或者更新了,接下来就是开始 commitBeforeMutationEffects 的方法

commitBeforeMutationEffects

喜闻乐见,18 中又改版了,但是改版的不多,就是把主逻辑移动到了 _begin 去

这里我们就拿 18 的来看:

export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
): boolean {
  /** 获取 focusedInstanceHandle */

  // 设置已经过 render 处理的 fiber 起点
  nextEffect = firstChild;
  commitBeforeMutationEffects_begin();

  // We no longer need to track the active instance fiber
  const shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;

  return shouldFire;
}

function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;

    // This phase is only used for beforeActiveInstanceBlur.
    // Let's skip the whole loop if it's off.
    if (enableCreateEventHandleAPI) {
      /** todo!: 补充 */
    }

    const child = fiber.child;
    // 有子节点且需要更新
    if (
      (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
      child !== null
    ) {
      child.return = fiber;
      nextEffect = child;
    } else {
      // 走到底部了,开始归过程
      commitBeforeMutationEffects_complete();
    }
  }
}

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    try {
      // 方法入口
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      captureCommitPhaseError(fiber, fiber.return, error);
    }

    // 去到兄弟节点
    const sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }

    // 否则向上归
    nextEffect = fiber.return;
  }
}

commitBeforeMutationEffectsOnFiber 首先是处理 DOM 节点渲染/删除后的 autoFocus、blur 逻辑,然后执行了向 useEffect 队列内推入事件的操作

这里只看 useEffect(都什么年代了还写传统组件

manage useEffect

switch (finishedWork.tag) {
  case FunctionComponent: {
    if (enableUseEffectEventHook) {
      if ((flags & Update) !== NoFlags) {
        commitUseEffectEventMount(finishedWork);
      }
    }
    break;
  }
}

这里单纯把 useEffect 的事件挂载到了自身,真实调用则发生在 commitRootImpl 上,可能是为了更好维护代码吧。

其具体调用发生在 commitBeforeMutationEffects 后,在触发 useEffect 前

接下来就是 Mutation 阶段

mutation

从这里开始就切换参考文章了,毕竟 react 16 和 18 差距确实大,后面找了个 18 源码解析来看:https://juejin.cn/post/7329341975207936038#heading-7

commitMutationEffects -> commitMutationEffectsOnFiber

直接看比较重要的 commitMutationEffectsOnFiber

这个方法在针对不同的 fiber node 进行处理,单纯看 FunctionComponent 和 ClassComponent 就会发现他们都调用了:recursivelyTraverseMutationEffects and commitReconciliationEffects,而且默认处理中也会调用这两个方法

先看第一个方法:

function recursivelyTraverseMutationEffects(
  root: FiberRoot,
  parentFiber: Fiber,
  lanes: Lanes,
) {
  // 确认fiber节点是否有删除标记,有就进行dom删除操作
  const deletions = parentFiber.deletions;
  if (deletions !== null) {
    for (let i = 0; i < deletions.length; i++) {
      const childToDelete = deletions[i];
      try {
        // 进行删除,发生外部 dom 副作用,此处调用 useEffect 等钩子返回的 destory 函数
        commitDeletionEffects(root, parentFiber, childToDelete);
      } catch (error) {
        captureCommitPhaseError(childToDelete, parentFiber, error);
      }
    }
  }

  // 递归处理
  // 递归顺序与前文 render 相同,先到最深的叶子节点,然后再往上,如果有兄弟节点就往兄弟节点走,直到遍历完整一棵树
  if (parentFiber.subtreeFlags & MutationMask) {
    let child = parentFiber.child;
    while (child !== null) {
      commitMutationEffectsOnFiber(child, root, lanes);
      child = child.sibling;
    }
  }
}

MutationMask 内容如下:

export const MutationMask =
  Placement |
  Update |
  ChildDeletion |
  ContentReset |
  Ref |
  Hydrating |
  Visibility |
  FormReset;

这里可以看到是 fiber 节点的副作用类型:挂载 / 更新 / 子节点移除等。

接下来看第二个:

function commitReconciliationEffects(finishedWork: Fiber) {
  // Placement effects (insertions, reorders) can be scheduled on any fiber
  // type. They needs to happen after the children effects have fired, but
  // before the effects on this fiber have fired.
  const flags = finishedWork.flags;
  if (flags & Placement) {
    try {
      // 执行渲染方法,包含 dom 副作用
      commitPlacement(finishedWork); // 调用 insertOrAppendPlacementNode
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
    // Clear the "placement" from effect tag so that we know that this is
    // inserted, before any life-cycles like componentDidMount gets called.
    // TODO: findDOMNode doesn't rely on this any more but isMounted does
    // and isMounted is deprecated anyway so we should be able to kill this.
    finishedWork.flags &= ~Placement;
  }
  if (flags & Hydrating) {
    finishedWork.flags &= ~Hydrating;
  }
}

没什么好说的,这里判断是否有 Placement flag,有就进行插入或者移动操作

commitPlacement 就是具体执行这部分的代码,内部会根据节点类型来进行放置或移动操作,调用的是 insertOrAppendPlacementNode 该方法同样递归执行,并调用 insertBeforeappendChild 来执行 dom 操作,这里具体的代码可以在 ReactFiberConfigDOM.js 看到

switch fiber tree

在渲染完成后则进行 fiber tree 的切换,引用一下参考文章的话:

之所以选择这一时机切换fiber tree, 是因为对于ClassComponent,当执行componentWillUnmount(mutation阶段)的时候,current fiber tree 仍对应UI中的树,当执行componentDidMount/Update(Layout阶段)的时候,current fiber tree就对应本次更新的fiber tree了,也就是原来的 wip fiber tree 变成了 current fiber tree

代码原注释:

The work-in-progress tree is now the current tree. This must come after the mutation phase, so that the previous tree is still current during componentWillUnmount, but before the layout phase, so that the finished work is current during componentDidMount/Update.

大致意思与前文差不多,都是为了解决 componentWillUnmount 时 fiber 树是否对应旧数据的问题

Layout

然后就是提交 Layout 副作用 commitLayoutEffects

commitLayoutEffects -> commitLayoutEffectOnFiber

export function commitLayoutEffects(
  finishedWork: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
): void {
  inProgressLanes = committedLanes;
  inProgressRoot = root;

  const current = finishedWork.alternate;
  commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);

  inProgressLanes = null;
  inProgressRoot = null;
}

commitLayoutEffectOnFiber

与前文 mutation 阶段一样,判断当前 fiber node 类型,然后根据类型调用不同方法,一般是走 recursivelyTraverseLayoutEffects ,然后调用 commitHookLayoutEffects

假如 fiber node 是函数组件,则同步执行 useLayoutEffect 回调

假如是类组件,则执行以下方法:

  • commitClassLayoutLifecycles - 调用类组件 componentDidMount | componentDidUpdate 函数,这个判断通过 current fiber !== null 来确认是挂载还是更新
  • commitClassCallbacks - 更新队列副作用 callback
  • safelyAttachRef - ref 处理

先看第一个函数:

function recursivelyTraverseLayoutEffects(
  root: FiberRoot,
  parentFiber: Fiber,
  lanes: Lanes,
) {
  if (parentFiber.subtreeFlags & LayoutMask) {
    let child = parentFiber.child;
    while (child !== null) {
      const current = child.alternate;
      commitLayoutEffectOnFiber(root, current, child, lanes); // 回到判断类型执行方法这一步
      child = child.sibling;
    }
  }
}

还是递归处理节点,这里唯一要提的就是 LayoutMask,具体内容如下:

export const LayoutMask = Update | Callback | Ref | Visibility;

LayoutMask 就是代表 Layout 阶段所需要执行的哪些副作用类型

然后看 commitHookLayoutEffects 这里就是执行生命周期钩子的地方,内部仅仅调用 commitHookEffectListMount 这个方法:

function commitHookEffectListMount(/** constants on call: HookLayout | HookHasEffect */flags: HookFlags, finishedWork: Fiber) {
  const updateQueue = finishedWork.updateQueue;
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  // 副作用队列不为 null
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        /** schedule profiler */

        // Mount
        const create = effect.create;

        const inst = effect.inst;
        // 调用 useLayoutEffect 传入的函数
        const destroy = create();
        // 设置清理函数
        inst.destroy = destroy;

        /** schedule profiler */
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

注释内都很详细了,这里就不多说啥了

总结

在 commit Layout 环节中做了以下工作:

  • 调用 useLayoutEffect 副作用函数
  • 绑定 / 更新 ref
个人信息
avatar
Shiinafan
文章
42
标签
52
归档
4
导航