React源码阅读(四)- commit

  • 走完render之后自然就进入了我们的commit阶段。 我们在文章二也阐述了入口为finishConcurrentRender

一、finishConcurrentRender

这里主要是根据exitStatus的不同情况进行了switch case的判断, 我们直接找到我们需要的Case: RootCompleted。可以看到直接调用了commitRoot

js 复制代码
function finishConcurrentRender(root, exitStatus, lanes) {
  switch (exitStatus) {
    case RootInProgress:
    case RootFatalErrored: ....
    case RootErrored: ....
    case RootSuspended: ....
    case RootSuspendedWithDelay: .....
    case RootCompleted: {
      // The work completed. Ready to commit.
      commitRoot(
        root,
        workInProgressRootRecoverableErrors,
        workInProgressTransitions,
      );
      break;
    }
    default: {
      throw new Error('Unknown root exit status.');
    }
  }
}
  • 我们继续看commitRoot, 可以看到直接调用了commitRootImpl

二、 commitRootImpl

commitRootImpl整体可以分为三个阶段

  • commit准备阶段
  • commit阶段: commit阶段会把render阶段计算出的变更应用到DOM上。 具体可以分为三个子阶段
    • before mutation: 操作DOM之前的操作。
    • mutation: 主要是操作DOM, 比如更新移动新增
    • layout: 在DOM操作之后操作
  • commit结束阶段(这篇篇幅太长了, 这个先跳过)

三、commit准备阶段

commit正式开始之前的逻辑我们通通归纳到准备阶段。 可以看到一开始就来了个循环去调用flushPassiveEffects。 然后是一些对状态的设置,重置。 然后又搞了个调度去调用flushPassiveEffects。核心就是: 清理, 重置, 调度。 具体可见注释

js 复制代码
// 开启do while循环去处理副作用
do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);
  
  ...
  // 拿到render阶段结束后的成果
  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;
  // 重置root的状态
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  // 清空当前任务, 因为commit是不可打断是同步的, 故可以直接清除, 方便后续安排新的调度
  root.callbackNode = null;
  root.callbackPriority = NoLane;
  // 合并hostRootFiber的lanes和其子孙节点的lanes拿到剩余的待处理的优先级
  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  // 因为存在并发机制,故存在updateLanes, 此时不能标记其为已完成, 故也需要合并进去
  const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
  remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
  // 处理eventTimes和expirationTimes等状态
  markRootFinished(root, remainingLanes);
  // 在render最后的处理中,我们都将workInProgressRoot置为空了
  // 这里对其再次进行判断, 如果render阶段没有成功置空的话这里进行置空
  if (root === workInProgressRoot) {
    workInProgressRoot = null;
    workInProgress = null;
    workInProgressRootRenderLanes = NoLanes;
  } 
  // Passive标记对应的是useEffect的副作用操作
  // 如果存在挂起的副作用, 那么则通过scheduleCallback生成一个task任务去处理
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      pendingPassiveEffectsRemainingLanes = remainingLanes;
      pendingPassiveTransitions = transitions;
      // 通过scheduleCallback调度了一个宏任务去处理flushPassiveEffects。 
      // 具体逻辑可以看第二章, 其中就涉及了该函数
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;  
      });
    }
  }

flushPassiveEffects

具体的flushPassiveEffects我们后面接触函数组件再去仔细了解

在上面的代码中我们一共两次调用了该函数。 我们分为前后去介绍这两个函数

  • 第一个: 因为我们前前后后可能执行了多个commit。那么我们需要保证本次commit的副作用在下一次commit开启之前全部执行完。 故需要在每一次的commit流程开始之前, 需要先调用一次flushPassiveEffects清空上一次commit可能遗留下来的副作用
  • 第二个: 我们是通过schduleCallback的方法进行调度, 因为commit的逻辑是同步不可打断的。 故通过schduleCallback调度后就可以保证flushPassiveEffects是在DOM渲染完成之后执行的(本质上是利用了MessageChannel, 具体可见文章二)

markRootFinished

可以看到逻辑上就是根据pendinglanesremainingLanes拿到noLongerPendingLanes. 即为已经完成任务的优先级。 然后通过while循环去重置对应赛道下标的eventTimesexpirationTimesentanglements

js 复制代码
export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
  // 通过是释放的操作, 拿到已经完成的(不再等待的)优先级
  const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
  // 赋值给root的pendinglanes
  root.pendingLanes = remainingLanes;
  ....
  // 根据noLongerPendingLanes清理已经处理完的任务
  let lanes = noLongerPendingLanes;
  while (lanes > 0) {
    const index = pickArbitraryLaneIndex(lanes);
    const lane = 1 << index;
    entanglements[index] = NoLanes;
    eventTimes[index] = NoTimestamp;
    expirationTimes[index] = NoTimestamp;
    ....
    lanes &= ~lane;  // 继续找下一个处理
  }
}

四、commit阶段

commit阶段的流程不是一定进行的, 会先进行判断flags。一共是判断了四种flagsBeforeMutationMask MutationMaskLayoutMasK, PassiveMask

  • 这里比较了hostRootFiberflags以及subtreeFlags。 因为我们之前在completeWork进行了flags的冒泡处理, 故这里可以直接通过finishedWork.subtreeFlags进行判断,而不是遍历每一个子节点。 准备阶段中判断PassiveMask的逻辑同理。
  • 如果存在副作用的话就走入真正的commit阶段, 没有的话就直接切换树
js 复制代码
 // 检查构造好的Fiber的子孙节点是否存在副作用需要操作
  const subtreeHasEffects =
    (finishedWork.subtreeFlags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;
 // 检查hostRootFiber本身是否存放副作用需要进行操作
  const rootHasEffect =
    (finishedWork.flags &
      (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;

 // 只要存在副作用, 那么则进入commit阶段
  if (subtreeHasEffects || rootHasEffect) {
      ...
  } else {
    // 没有副作用的话直接切换树了
    root.current = finishedWork;
  }

真正的commit逻辑如下。 先来一波一览众山小, 总的逻辑是非常清晰的。 先记录了优先级和上下文,然后修改为commit阶段所需要的内容, 等commit的三个阶段完成之后进行重置。其中

  • 调用了commitBeforeMutationEffects进入beforeMutation阶段。
  • 调用commitMutationEffects进入了mutation阶段
  • 调用commitLayoutEffects进入了layout阶段
  • 最后调用了requestPaint让浏览器进行渲染
js 复制代码
 ReactCurrentBatchConfig.transition = null;
 // 记录了当前的优先级
 const previousPriority = getCurrentUpdatePriority();
 // 将当前的优先级设置为同步优先级(最高)
 setCurrentUpdatePriority(DiscreteEventPriority);
 // 记录当前上下文
 const prevExecutionContext = executionContext;
 // 更新上下文为CommitContext
 executionContext |= CommitContext;
 ReactCurrentOwner.current = null;

-------------beforeMutation----------------------------------
 const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
   root,
   finishedWork,
 );
......
--------------mutation阶段------------------------------------
commitMutationEffects(root, finishedWork, lanes);

// 这里重置了containerInfo相关信息
resetAfterCommit(root.containerInfo);

// 切换workInProgress树到current树
// 因为当前的Fiber和DOM已经算更新完了,故对于下一次更新而言,
// 也需要拿到当前当前页面使用的Fiber结构。 故需要进行切换
root.current = finishedWork;


---------------layout阶段-----------------------------------------
commitLayoutEffects(finishedWork, root, lanes);

// 通过浏览器开始渲染
requestPaint();

// 将上下文以及优先级恢复
executionContext = prevExecutionContext;
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;

五、before mutation

先看入口函数commitBeforeMutationEffects, 传入的root为根应用节点, firstChildrender阶段构造好的Fiber树。 首先将nextEffect初始化为传入的Fiber树, 接着可以看到他调用了关键函数commitBeforeMutationEffects_begin(看名字就知道他关键)

ts 复制代码
export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
) {
  focusedInstanceHandle = prepareForCommit(root.containerInfo);
  nextEffect = firstChild;
  commitBeforeMutationEffects_begin();
  const shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;

  return shouldFire;
}

commitBeforeMutationEffects_begin

大概扫一眼你就能发现这函数里面还调用commitBeforeMutationEffects_complete这个东西, 诶这不就跟前面所了解的beginworkcompleteWork的命名很像吗. 看来,这个也是去找寻Fiber树的过程

我们一起探究下这像不像那回事。 该函数主要通过while循环, 基于substressFlags找到节点符合BeforeMutationMask的。 总体通过Child链路从头找到尾

ts 复制代码
function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect; // 当前的Fiber
    const child = fiber.child; // 记录下的他的Child
    // 如果他存在子节点且他的子节点存在flags标记符合BeforeMutationMask
    // 那么就跳过继续向下找
    if (  
      (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
      child !== null
    ) {
      child.return = fiber;
      nextEffect = child; // 那么就继续找他的子节点
    } else {
      // 不符合上述条件时即调用
      commitBeforeMutationEffects_complete();
    }
  }
}

commitBeforeMutationEffects_complete

这个函数进来也是一个while寻找, 判断条件依旧是nextEffect

js 复制代码
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; // 此时直接return出去, 又会走到commitBeforeMutationEffects_begin的逻辑
    }
    // 如果不存在兄弟节点的话那么往回走
    nextEffect = fiber.return;
  }
}

commitBeforeMutationEffectsOnFiber

可以看到该函数主要就是处理了flags存在Snapshot的节点。 只要满足(flags & Snapshot) !== NoFlags才会走入真正的操作逻辑。 其中的逻辑又是通过tag去对不同节点做不同的处理。 当然Snapshot主要还是考虑类组件和hostRootFiber

主要有两个地方会打上Snapshot标签

  • 第一是因为class组件存在getSnapshotBeforeUpdate方法。 具体可看beginWork-> updateClassComponet -> updateClassInstance
  • 第二是completeWorkCase HostRoot的时候。 其注释也给出了原因--> 为了在commit的初始阶段时清空container

对于类组件我们可以回顾一下的getSnapshotBeforeUpdate的用法并思考如何处理

  • getSnapshotBeforeUpdate() 方法在提交到 DOM 节之前调用---------刚好就是beforeMutation
  • getSnapshotBeforeUpdate() 方法中,我们可以访问更新前的 propsstate---------需要拿到之前的stateprops作为参数传入
  • 其返回值将作为参数传递给componetDidUpdate()-----需要拿到该函数的返回值并记录

具体看看commitBeforeMutationEffectsOnFiber是如何处理的

js 复制代码
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate; 
  const flags = finishedWork.flags; // 当前节点的flags标志
  // 只要标志为Snapshot才会进行处理
  if ((flags & Snapshot) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        break;
      }
      // 对于类组件
      case ClassComponent: {
        if (current !== null) {
          const prevProps = current.memoizedProps;  // 拿到之前的props
          const prevState = current.memoizedState;  // 拿到之前的state
          const instance = finishedWork.stateNode;  // 拿到组件实例
          // 在这里调用了getSnapshotBeforeUpdate, 返回值赋值给snapshot
          const snapshot = instance.getSnapshotBeforeUpdate( 
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          // 然后赋值给instance.__reactInternalSnapshotBeforeUpdate进行保存
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
        break;
      }
      // 对于hostFiberRoot
      case HostRoot: {
       if (supportsMutation) {
         const root = finishedWork.stateNode;  // 拿到Root根应用节点
         // root.containerInfo执行根DOM节点, 此处调用clearContainer进行清空处理
         clearContainer(root.containerInfo);
       }
       break;
      }
      .....
  }
}

小总结

这一段的逻辑还是相对简单很多的, 因为目标很明显且单一。 就是基于subtreeFlags查找Fiber树,然后找到flags带有Snapshot的节点进行处理。 如果是类组件则调用其getSnapshotBeforeUpdate函数。 如果是hostFiberRoot则对根DOM节点进行清空处理

我顺着逻辑画了个图, 可以注意到几个点

  • 如果子节点不存在BeforeMutationMask是不会继续往下走的,例如图右下的节点,他会直接找他的兄弟节点, 若没有的话则直接回父节点了, 故不是所有的节点都会调用commitBeforeMutationEffectsOnFiber方法。
  • commitBeforeMutationEffectsOnFiber调用的顺序有点类似我们常说的二叉树的中序遍历, 先找到底,处理完其子节点调用, 再回到它本身调用
  • xxxbeiginxxxcomplete的调用逻辑和之前的beiginWorkcompleteWork调用逻辑很像。都是靠beigin往深处挖掘,然后靠complete找兄弟节点和往回走

这个图建议多看看。React很多个地方都采用了这个逻辑。包括后面的layout。 争取举一反三

六、mutation

先看入口函数commitMutationEffects, 这里没有什么逻辑, 就是设置了inProgressLanesinProgressRoot, 然后调用了commitMutationEffectsOnFiber, 调用完再清空

ts 复制代码
export function commitMutationEffects(
  root: FiberRoot,
  finishedWork: Fiber,
  committedLanes: Lanes,
) {
  // 调用前设置优先级和root
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
  // 调用完置空
  inProgressLanes = null;
  inProgressRoot = null;
}

commitMutationEffectsOnFiber

主体逻辑还是通过Switch Case对不同的节点进行了不同的处理。 这里比较冗长我直接都省略了。但是无论tag是什么, 一定有两个函数会执行,可以看到正是default里面的recursivelyTraverseMutationEffectscommitReconciliationEffects。 我们先了解一下这两个函数, 后面再细分tag去了解

ts 复制代码
function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate; // 拿到当前页面使用的Fiber结构
  const flags = finishedWork.flags; // 拿到当前节点的标签

  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: ......
    case ClassComponent: ....
    case HostComponent: .....
    case HostText: .....
    case HostRoot: ....
    case HostPortal: ....
    case SuspenseComponent: ....
    case OffscreenComponent:....
    case SuspenseListComponent: ....
    case ScopeComponent: ......
    default: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);

      return;
    }
  }
}

recursivelyTraverseMutationEffects

  • 从函数命名-递归遍历mutation副作用。 就知道这必为核心函数。 故我们要比较细致地去看这个函数
  • 他分成两部分
    • 优先处理了deletions(进行其他操作也需要循环Fiber树,此时如果有该被删除的节点但是还没有被删除的,容易有意外的结果. 所以我们在操作一个Fiber节点时,一定是先考虑删除操作),
      • OK我们先看一下render阶段对于删除节点我们是如何处理的:
        • 主要是父节点flags打上了ChildDeletion(这里用于匹配MutationMask, 让其能够进入commit流程)
        • 然后父节点的deletetions属性push进去需要删除的节点(这里用于拿到具体的目标删除节点--这里是旧节点哦)
      • commit阶段这里采用了commitDeletionEffects作为删除的入口
    • 然后再通过while去处理符合MutationMask的节点, 递归的思想就体现在这里, 这里的逻辑是顺着Child链和Sibling链, 然后去遍历到每一个子节点, 然后又调用了我们前面讲述的commitMutationEffectsOnFiber, 形成了递归
js 复制代码
function recursivelyTraverseMutationEffects(
  root: FiberRoot,
  parentFiber: Fiber,
  lanes: Lanes,
) {
  // 删除类型可以安排在任何类型的Fiber节点上。需要优先被处理
  const deletions = parentFiber.deletions;
  if (deletions !== null) {  // 如果没有需要删除的节点的话这里为null, 有的话才会是数组
    // 遍历需要删除的节点
    for (let i = 0; i < deletions.length; i++) {
      const childToDelete = deletions[i]; 
      try {
        commitDeletionEffects(root, parentFiber, childToDelete);
      } catch (error) {
        captureCommitPhaseError(childToDelete, parentFiber, error);
      }
    }
  }
  // 如果子节点flgas满足MutationMask, 那么又开启了递归遍历进行处理
  if (parentFiber.subtreeFlags & MutationMask) {
    let child = parentFiber.child;
    while (child !== null) {
      commitMutationEffectsOnFiber(child, root, lanes);
      child = child.sibling;
    }
  }
}

Deletion

删除的部分会比较详细地讲解, 主打一个举一反三

commitDeletionEffects

该函数为删除的入口函数,这里使用标签语句,从目标删除节点开始, 顺着你return找其父节点。 在Case找到目标tag标签的时候, 记录下stateNode(DOM节点)和设置hostParentIsContainer状态,然后退出Switch且退出while。 接着调用了commitDeletionEffectsOnFiber

故这个函数的作用很明显, 就是找到hostParent。 因为我们的父Fiber节点可能是ClassComponent, 可能是FunctionComponent,这些节点是无法直接操作DOMAPI的, 故我们会找到最近的能够操作的父节点, 拿到他们的stateNode, 也就是我们之前在render阶段的CompleteWork中生成的DOM节点

js 复制代码
function commitDeletionEffects(
  root: FiberRoot,
  returnFiber: Fiber,
  deletedFiber: Fiber,
) {
 let parent = returnFiber;
 findParent: while (parent !== null) {
   switch (parent.tag) {
     case HostComponent: {
       hostParent = parent.stateNode;
       hostParentIsContainer = false;
       break findParent;
     }
     case HostRoot: {
       hostParent = parent.stateNode.containerInfo;
       hostParentIsContainer = true;
       break findParent;
     }
     case HostPortal: {
       hostParent = parent.stateNode.containerInfo;
       hostParentIsContainer = true;
       break findParent;
     }
   }
   parent = parent.return;
 }
 
 commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
 hostParent = null;
 hostParentIsContainer = false;
  detachFiberMutation(deletedFiber);
}
commitDeletionEffectsOnFiber

这个函数就是实际上操作删除的函数了, 这里依旧是通过区分不同的tag执行不同的方法, 我们简单了解一下几种常见的节点。

js 复制代码
function commitDeletionEffectsOnFiber(
  finishedRoot: FiberRoot,
  nearestMountedAncestor: Fiber,
  deletedFiber: Fiber,
) {
  onCommitUnmount(deletedFiber);
  switch (deletedFiber.tag) {
    case HostComponent: .....
    case HostText: .....
    case DehydratedFragment: ...
    case HostPortal: ....
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: ....
    case ClassComponent: ....
    case ScopeComponent: ...
    case OffscreenComponent: ...
    default: {
      recursivelyTraverseDeletionEffects(
        finishedRoot,
        nearestMountedAncestor,
        deletedFiber,
      );
      return;
    }
  }
}
recursivelyTraverseDeletionEffects

基本上无论走到哪一步都会调用该函数, 我们先了解一下该函数的作用. 可以看到这里这一步核心思想就是。 诶我要被删除了, 那我的子节点们也得一个不留,通通删除。 又形成了递归。 跟recursivelyTraverseMutationEffects中的逻辑没啥两样的

再说准确一点,这里的删除其实不是对DOM的操作。 而是需要通知到每一个节点。 也许他们存在什么删除前的逻辑需要处理(dddd)-- 请看后续讲解

js 复制代码
function recursivelyTraverseDeletionEffects(
  finishedRoot,  // 根应用节点
  nearestMountedAncestor, // Fiber父节点
  parent, // 目标删除节点
) {
  let child = parent.child;  // 找到第一个子节点
  while (child !== null) { // 删除所有子节点
    // 通过调用commitDeletionEffectsOnFiber去挨个删除子节点
    commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, child);
    child = child.sibling; // 所有子节点
  }
}
hostText/hostComponent

这里是Switch Case针对于taghostText的措施。 也可以说是分为上下两部分, 上面是递归的逻辑, 下面是通过removeChild实现真正DOM操作删除节点的逻辑

js 复制代码
  const prevHostParent = hostParent; // 先记录其DOM父节点
  const prevHostParentIsContainer = hostParentIsContainer;
  hostParent = null; // 在递归开始前置空
  // 一开始就先执行了递归删除子节点, 跳出了说明子节点为null了
  recursivelyTraverseDeletionEffects(
    finishedRoot,
    nearestMountedAncestor,
    deletedFiber,
  );
  // 递归结束之后设置回来
  hostParent = prevHostParent;
  hostParentIsContainer = prevHostParentIsContainer;
  // 因为递归开始前将其置空了,跳出后有值。 故当hostParent不为空的时候说明此时deletedFiber Child为空
  if (hostParent !== null) {
    // 这里核心都是通过调用removeChild去操作DOM移出节点
    if(hostParentIsContainer) {
      removeChildFromContainer(
        ((hostParent: any): Container),
        (deletedFiber.stateNode: Instance | TextInstance),
      );
    } else {
      removeChild(
        ((hostParent: any): Instance),
        (deletedFiber.stateNode: Instance | TextInstance),
      );
    }
  }
return;

来个实例走一遍流程,我们对节点进行卸载, 旧节点树和新节点树如图所示 我们通过打断点, 找到此时的情况, 开始走入删除流程。 找hostParent这一步我们直接跳过, 因为parentFiber本身就是一个hostComponent, 最后hostParent就是指向他的stateNode

接着走到commitDeletionEffectsOnFiber -> recursivelyTraverseDeletionEffects进入递归。 此时会扫描其子节点。 直接Child为空跳出了递归, 但是除了第一次进入recursivelyTraverseDeletionEffects的上下文之外, 其他上下文拿到的hostParent其实都为空。 故他们也不做删除节点的事。就只有我们目标删除的fiber节点会采用removeChild的逻辑。

那么我们为什么还要去遍历到每一个子节点呢? 我们带着问题走入FunctionComponentClassComponent的逻辑。 走入之前我们先改一下我们的Fiber树

ClassComponent

可以看到这里的逻辑和hostText有明显的不同, 多了一部分也少了一部分

  • 少掉的: 少掉的是操作DOM API的逻辑, 因为Fiber tagclassComponent的时候是不会对应真实DOM结构的, 故这里不需要对应逻辑
  • 多出来的, 可以看到主要有两个函数safelyDetachRefsafelyCallComponentWillUnmount, 我们放在下面详细讲解
js 复制代码
 if (!offscreenSubtreeWasHidden) {
    safelyDetachRef(deletedFiber, nearestMountedAncestor);
    const instance = deletedFiber.stateNode;
    if (typeof instance.componentWillUnmount === 'function') {
      safelyCallComponentWillUnmount(
        deletedFiber,
        nearestMountedAncestor,
        instance,
      );
    }
  }
  recursivelyTraverseDeletionEffects(
    finishedRoot,
    nearestMountedAncestor,
    deletedFiber,
  );
  return;
}
  • 对于safelyDetachRef, 直接上图, 从前面代码可以得知, 我们用ref拿了该类组件的实例。 当该类组件要被销毁的时候, 此时应该将ref.current重置为null.(ref还可以是函数, 处理逻辑会更复杂, 这里直接跳过了)。 反正该函数核心目标是处理ref让他拿到正确的值
  • 对于safelyCallComponentWillUnmount, 看名字就知道个七七八八了, 要卸载一个类组件, 如果该类组件设置了ComponentWillUnmount的时候, 那此时就应该拿出来执行了

FunctionComponent

函数组件的逻辑相比其类组件就要复杂一些了。 但是万变不离其宗。 他也是做了一些组件卸载前需要处理的回调。 后面会针对React hook出一篇。 这里就直接简单描述下

我们在该函数组件使用了useEffect, 也使用了useLayoutEffect

这里会去遍历我们的updateQueue, 然后对tag进行识别, 对于useEffect来说tag9,此时不会走入逻辑中, 对于useLayoutEffect来说tag5, 此时会调用safelyCallDestroy, 该函数会调用destroy, 如图, 也就是我们useLayoutEffect return出去的函数。 所以我们说useLayoutEffect是同步的。 如果在这里做过多的操作的话那么就会阻塞整个commit的进行

commitReconciliationEffects

讲了一大堆recursivelyTraverseMutationEffects后, 让我们回到recursivelyTraverseMutationEffects的下一个函数commitReconciliationEffects.

看到flags判断Placement就知道这个是用来处理插入节点相关的逻辑。 他可以处理添加和插入。

js 复制代码
function commitReconciliationEffects(finishedWork: Fiber) {
  const flags = finishedWork.flags;
  if (flags & Placement) {
    try {
      commitPlacement(finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
   
    finishedWork.flags &= ~Placement;
  }
}

我们再继续看commitPlacement, 这里只有两种类型。 一种就普通的节点。 可以处理插入或者添加节点的操作, 通过insertOrAppendPlacementNode。 这里的逻辑多少和删除有点接近, 就不赘述了

再看对hostRoothootPortal的处理.

  • 还记得我们曾在render阶段, 初次渲染的时候通过placeSingleChildhostRoot打上了Placemenet的标签(文章三讲述过,可以回看)。 这里就是将他拿出来使用的时候
js 复制代码
 case HostRoot:
 case HostPortal: {
   const parent: Container = parentFiber.stateNode.containerInfo;
   const before = getHostSibling(finishedWork);
   insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
   break;
 }
  • insertOrAppendPlacementNodeIntoContainer。 该函数也是先顺着Child寻找, 找到目标的HostComponent或者HostText。 然后直接将对应的stateNode插入根DOM节点(因为这里传入的parent就是根节点)
js 复制代码
function insertOrAppendPlacementNodeIntoContainer(
  node: Fiber,
  before: ?Instance,
  parent: Container,
): void {
  const {tag} = node;
  const isHost = tag === HostComponent || tag === HostText;
  if (isHost) {
    const stateNode = node.stateNode;
    if (before) {
      insertInContainerBefore(parent, stateNode, before);
    } else {
      appendChildToContainer(parent, stateNode);
    }
  } else if (tag === HostPortal) {
    // If the insertion itself is a portal, then we don't want to traverse
    // down its children. Instead, we'll get insertions from each child in
    // the portal directly.
  } else {
    const child = node.child;
    if (child !== null) {
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
      let sibling = child.sibling;
      while (sibling !== null) {
        insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

可以看到该函数被调用后, 所有的节点都挂载上去了。为什么挂了一个就能全部挂上去了呢---------> 在render阶段的CompleteWork中, 我们就生成了DOM并让他形成了对应的DOM树。 故就像一个葡萄树, 我们把枝头给挂起来, 那么整串就都挂上了

看完上面的逻辑。再看下面的两个图就很清晰了

  • 首次渲染performance图--commit过程
  • 触发了节点的卸载对应的performance图---commit过程

小总结

回看mutation过程, 主要流程如下

  • 操作DOM, 包括增删改。 在首次渲染的时候还负责将DOM树挂载上去
  • 对于类组件, 在组件卸载的时候调用了ComponentWillUnMount以及重置了ref
  • 对于函数组件, 在组件卸载的时候调用了useLayoutEffectdestory函数

七、layout

还是先看入口函数commitLayoutEffects. 可以看到又是先状态初始化, 然后又是xxxxbegin, 调用完再重置状态。 熟悉的套路(扶额笑扶额笑扶额笑)

js 复制代码
export function commitLayoutEffects(
  finishedWork: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
): void {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  nextEffect = finishedWork;

  commitLayoutEffects_begin(finishedWork, root, committedLanes);

  inProgressLanes = null;
  inProgressRoot = null;
}

commitLayoutEffects_begin

这个整体套路和commitBeforeMutationEffects_begin基本一致, 不清楚的可以直接回看之前的函数。 我们这里不赘述了。

js 复制代码
function commitLayoutEffects_begin(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  // Suspense layout effects semantics don't change for legacy roots.
  const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const firstChild = fiber.child;
    ...
    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      firstChild.return = fiber;
      nextEffect = firstChild;
    } else {
      commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
    }
  }
}

commitLayoutMountEffects_complete

不赘述+1、我们直接看commitLayoutEffectOnFiber想干什么

js 复制代码
function commitLayoutMountEffects_complete(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    if ((fiber.flags & LayoutMask) !== NoFlags) {
      const current = fiber.alternate;
      try {
        commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
      } catch (error) {
        captureCommitPhaseError(fiber, fiber.return, error);
      }
    }
    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }
    const sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }
    nextEffect = fiber.return;
  }
}

commitLayoutEffectOnFiber

这个函数直接拉了坨大的。 整体代码量较多,直接看比较经典的函数组件和类组件即可。 我把核心逻辑都提出来了

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: {
           .....
         // 对于函数组件来说, 同步去执行useLayoutEffect的回调
        commitHookEffectListMount(
                HookLayout | HookHasEffect,
                finishedWork,
              );
             .....
        break;
      }
      case ClassComponent: {
        const instance = finishedWork.stateNode;
        if (finishedWork.flags & Update) {
          if (!offscreenSubtreeWasHidden) {
            if (current === null) {
              ...
              // 如果是初次挂载的话, 调用componentDidMount
              instance.componentDidMount();
            } else {
            // 如果是更新的话, 那么则调用componentDidUpdate
            // 这里传入了instance.__reactInternalSnapshotBeforeUpdate
            // 看commitBeforeMutationEffectsOnFiber。 我们在遇到getSnapshotBeforeUpdate的时候处理的
              const prevProps =
                finishedWork.elementType === finishedWork.type
                  ? current.memoizedProps
                  : resolveDefaultProps(
                      finishedWork.type,
                      current.memoizedProps,
                    );
              const prevState = current.memoizedState;
              ....
              instance.componentDidUpdate(
                    prevProps,
                    prevState,
                    instance.__reactInternalSnapshotBeforeUpdate,
               );
            }
          }
        }
        // 调用setState的回调
        const updateQueue: UpdateQueue<
          *,
        > | null = (finishedWork.updateQueue: any);
  
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
      case HostRoot: ...
      case HostComponent: ....
      case HostText: {

        break;
      }
      case HostPortal: {
   
        break;
      }
      case Profiler: .....
      case SuspenseComponent: ...
      case SuspenseListComponent:
      case IncompleteClassComponent:
      case ScopeComponent:
      case OffscreenComponent:
      case LegacyHiddenComponent:
      case TracingMarkerComponent: {
        break;
      }

      default:
        throw new Error(
          'This unit of work tag should not have side-effects. This error is ' +
            'likely caused by a bug in React. Please file an issue.',
        );
    }
  }
   // 进行Ref的绑定逻辑
  if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
    if (enableScopeAPI) {
      if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
        commitAttachRef(finishedWork);
      }
    } else {
      if (finishedWork.flags & Ref) {
        commitAttachRef(finishedWork);
      }
    }
  }
}
  • 可以看到上面的函数最后是进行了Ref的绑定。 调用的函数是commitAttachRef
js 复制代码
function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    // 实际上就是拿到他的stateNode, 也就是他对应的节点。
    // 如果是类组件的话就是对应的实例。 函数组件为null
    // 普通节点的话就是对应的DOM节点
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
   // ref是个函数的话则调用。 传入拿到的instanceToUse
    if (typeof ref === 'function') {
      let retVal;
      .. 
      retVal = ref(instanceToUse);
    } else {
      ref.current = instanceToUse; // 否则的话直接赋值给current即可
    }
  }
}

小总结

因为before mutationmutation了解得比较多。 故到layout阶段很多逻辑其实就很清晰了。我们也就是说简单地过了一下

所以它干了啥呢

  • 针对函数组件, 调用了useLayoutEffect的回调
  • 针对类组件, 初次挂载的情况下调用componentDidMount, 更新的情况下调用componentDidUpdate。 以及处理了setState的回调
  • 处理了Ref对象的绑定

这四篇文章整体过了一遍React的基本执行逻辑,整理起来收获还是挺大的,也输出了一些逻辑图。 当然可能会存在一些描述不够恰当, 不够清晰的地方,毕竟整个源码的逻辑是很庞大的。 接下去会更细节地出一些文章比如hook的处理, 比如合成事件, 也可能会更贴近实际开发去看待源码等等等

相关推荐
CL_IN1 小时前
企业数据集成:实现高效调拨出库自动化
java·前端·自动化
浪九天2 小时前
Vue 不同大版本与 Node.js 版本匹配的详细参数
前端·vue.js·node.js
Java知识技术分享3 小时前
使用LangChain构建第一个ReAct Agent
python·react.js·ai·语言模型·langchain
qianmoQ3 小时前
第五章:工程化实践 - 第三节 - Tailwind CSS 大型项目最佳实践
前端·css
椰果uu3 小时前
前端八股万文总结——JS+ES6
前端·javascript·es6
微wx笑3 小时前
chrome扩展程序如何实现国际化
前端·chrome
~废弃回忆 �༄4 小时前
CSS中伪类选择器
前端·javascript·css·css中伪类选择器
CUIYD_19894 小时前
Chrome 浏览器(版本号49之后)‌解决跨域问题
前端·chrome
IT、木易4 小时前
跟着AI学vue第五章
前端·javascript·vue.js
薛定谔的猫-菜鸟程序员4 小时前
Vue 2全屏滚动动画实战:结合fullpage-vue与animate.css打造炫酷H5页面
前端·css·vue.js