React解析--commit阶段

516 阅读8分钟

概述

在render阶段的末尾会调用commitRoot(root),进入commit阶段,这里的root指的就是fiberRoot。然后会遍历render阶段生成的effectList,effectList上的Fiber节点保存着对应的props变化。之后会遍历effectList进行对应的dom操作和生命周期、hooks回调或销毁函数等。

commit阶段的主要工作(即renderer的工作流程)分为三部分:

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

在before mutation阶段之前和layout阶段之后还有些额外的工作,比如:useEffect的触发、优先级相关设置、ref解绑。下面是整个commit阶段的流程图,结合代码和图一起看会很清楚。

image-20220119193405008

commitRoot函数中其实是调用了commitRootImpl函数。

function commitRoot(root) {
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    ImmediateSchedulerPriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}

在commitRootImpl的函数中主要分三个部分:

  1. commitBeforeMutationEffects,commit阶段的前置工作
  2. commitMutationEffects,mutation阶段
  3. commitLayoutEffects,layout阶段(也就是mutation后)

mutation前置阶段

这个阶段主要做的事:

  1. 调用flushPassiveEffects执行完所有effect的任务
  2. 初始化相关变量
  3. 赋值firstEffect给后面遍历effectList用
do {
    // 调用flushPassiveEffects执行完所有effect的任务
    // 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

  // root指 fiberRootNode
  // root.finishedWork指当前应用的rootFiber
  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;

  // 重置变量 finishedWork指rootFiber
  root.finishedWork = null;
  // 重置优先级
  root.finishedLanes = NoLanes;
  // 重置Scheduler绑定的回调函数
  root.callbackNode = null;

  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  // 重置优先级相关变量
  markRootFinished(root, remainingLanes);

  // 清除已完成的discrete updates,例如:用户鼠标点击触发的更新
  if (rootsWithPendingDiscreteUpdates !== null) {
    if (
      !hasDiscreteLanes(remainingLanes) &&
      rootsWithPendingDiscreteUpdates.has(root)
    ) {
      rootsWithPendingDiscreteUpdates.delete(root);
    }
  }

  // 重置全局变量
  if (root === workInProgressRoot) {
    // We can reset these now that they are finished.
    workInProgressRoot = null;
    workInProgress = null;
    workInProgressRootRenderLanes = NoLanes;
  } else {
  }

  // 将effectList赋值给firstEffect
  // 由于每个fiber的effectList只包含他的子孙节点
  // 所以根节点如果有effectTag则不会被包含进来
  // 所以这里将有effectTag的根节点插入到effectList尾部
  // 这样才能保证有effect的fiber都在effectList中
  // rootFiber可能会有新的副作用 将它也加入到effectList
  let firstEffect;
  if (finishedWork.flags > PerformedWork) {
    if (finishedWork.lastEffect !== null) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else {
      firstEffect = finishedWork;
    }
  } else {
    firstEffect = finishedWork.firstEffect;
  }

可以看到,before mutation之前主要做一些变量赋值,状态重置的工作。

mutation的三个函数

这个阶段做的主要事:

  1. 遍历effectList
  2. 分别执行三个方法commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects执行对应的dom操作和生命周期

我们知道react有双缓存,我们在构建完workInProgress Fiber树之后会将fiberRoot的current指向workInProgress Fiber,让workInProgress Fiber成为current,这个步骤发生在commitMutationEffects函数和commitLayoutEffects之间。

function commitRootImpl(root, renderPriorityLevel) {
  // ....
  // 以上的代码属于before mutation
  if (firstEffect !== null) {
    //...
	do {
      //...
      commitBeforeMutationEffects();
    } while (nextEffect !== null);
    
	do {
      //...
      commitMutationEffects(root, renderPriorityLevel); //commitMutationEffects
    } while (nextEffect !== null);
    
  root.current = finishedWork; //切换current Fiber树
    
  do {
      //...
      commitLayoutEffects(root, lanes);//commitLayoutEffects
    } while (nextEffect !== null);
			//...
  }
}
  • componentWillUnmount在commitMutationEffects函数中执行,这时还可以获取之前的Update
  • componentDidMount和componentDidUpdate在commitLayoutEffects函数中执行,这个时候可以获取更新后的dom了。

commitBeforeMutationEffects

该函数属于mutation阶段前置阶段,主要做的事:

  1. 处理DOM节点渲染/删除后的 autoFocus、blur逻辑。

    function commitBeforeMutationEffects() {
      while (nextEffect !== null) {
        const current = nextEffect.alternate;
        if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
          // ...focus blur相关
          if ((nextEffect.flags & Deletion) !== NoFlags) {
            if (doesFiberContain(nextEffect, focusedInstanceHandle)) {
              shouldFireAfterActiveInstanceBlur = true;
              beforeActiveInstanceBlur();
            }
          } else {
            if (
              nextEffect.tag === SuspenseComponent &&
              isSuspenseBoundaryBeingHidden(current, nextEffect) &&
              doesFiberContain(nextEffect, focusedInstanceHandle)
            ) {
              shouldFireAfterActiveInstanceBlur = true;
              beforeActiveInstanceBlur();
            }
          }
        }
    }
    
  2. 调用getSnapshotBeforeUpdate生命周期钩子。

    function commitBeforeMutationEffects() {
      while (nextEffect !== null) {
    		// ...
        const flags = nextEffect.flags;
        // 调用getSnapshotBeforeUpdate
        if ((flags & Snapshot) !== NoFlags) {
          setCurrentDebugFiberInDEV(nextEffect);
          commitBeforeMutationEffectOnFiber(current, nextEffect);
          resetCurrentDebugFiberInDEV();
        }
    }
    

    commitBeforeMutationEffectOnFiber是commitBeforeMutationLifeCycle的别名。

    在该方法内会调用getSnapshotBeforeUpdate

    值得一提的是,从react16开始,componentWillXXX的钩子前面增加了UNSAFE_的前缀,究其原因,是因为Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。

    为此,getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以getSnapshotBeforeUpdate也是同步的,不会遇到多次调用的问题。

  3. 调度useEffect。

    scheduleCallback方法由Scheduler模块提供,用于以某个优先级异步调度一个回调函数。

    function commitBeforeMutationEffects() {
      while (nextEffect !== null) {
        // ...
    		// 调度useEffect
        if ((flags & Passive) !== NoFlags) {
          if (!rootDoesHavePassiveEffects) {
            rootDoesHavePassiveEffects = true;
            scheduleCallback(NormalSchedulerPriority, () => {
              // 触发useEffect
              flushPassiveEffects();
              return null;
            });
          }
        }
        nextEffect = nextEffect.nextEffect; //遍历effectList
      }
    }
    

    被异步调度的回调函数就是触发useEffect的方法flushPassiveEffects,那为什么是异步调度的呢?

    react文档,effect的执行时机:

    与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。

    可见,useEffect异步执行的原因主要是防止同步执行时阻塞浏览器渲染

commitMutationEffects

这个函数主要做的事:

  1. 根据ContentReset effectTag重置文字节点
  2. 更新ref
  3. 根据effectTag分别处理,其中effectTag包括(Placement | Update | Deletion | Hydrating)
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  // 遍历effectList
  while (nextEffect !== null) {

    const effectTag = nextEffect.effectTag;

    // 根据 ContentReset effectTag重置文字节点
    if (effectTag & ContentReset) {
      commitResetTextContent(nextEffect);
    }

    // 更新ref
    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);
      }
    }

    // 根据 effectTag 分别处理
    const primaryEffectTag =
      effectTag & (Placement | Update | Deletion | Hydrating);
    switch (primaryEffectTag) {
      // 插入DOM
      case Placement: {
        commitPlacement(nextEffect);
        nextEffect.effectTag &= ~Placement;
        break;
      }
      // 插入DOM 并 更新DOM
      case PlacementAndUpdate: {
        // 插入
        commitPlacement(nextEffect);

        nextEffect.effectTag &= ~Placement;

        // 更新
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // SSR
      case Hydrating: {
        nextEffect.effectTag &= ~Hydrating;
        break;
      }
      // SSR
      case HydratingAndUpdate: {
        nextEffect.effectTag &= ~Hydrating;

        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // 更新DOM
      case Update: {
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // 删除DOM
      case Deletion: {
        commitDeletion(root, nextEffect, renderPriorityLevel);
        break;
      }
    }

    nextEffect = nextEffect.nextEffect;
  }
}

根据不同的Tag执行不同的操作:

  1. Placement effect

    当Fiber节点含有Placement effectTag,说明该Fiber节点对应的DOM节点需要插入到页面中。

    调用的方法为commitPlacement。

    // 当Fiber节点含有Placement effectTag,意味着该Fiber节点对应的DOM节点需要插入到页面中。调用的方法为commitPlacement
    function commitPlacement(finishedWork: Fiber): void {
      if (!supportsMutation) {
        return;
      }
    
      // 获取父级DOM节点。其中finishedWork为传入的Fiber节点
      const parentFiber = getHostParentFiber(finishedWork);
    
      // Note: these two variables *must* always be updated together.
      let parent;
      let isContainer;
      const parentStateNode = parentFiber.stateNode;
      switch (parentFiber.tag) {
        case HostComponent:
          ....
        case HostRoot:
          ...
        case HostPortal:
          ...
        case FundamentalComponent:
          ...
        default:
      }
      if (parentFiber.flags & ContentReset) {
        // Reset the text content of the parent before doing any insertions
        resetTextContent(parent);
        // Clear ContentReset from the effect tag
        parentFiber.flags &= ~ContentReset;
      }
    
      // 获取Fiber节点的DOM兄弟节点
      const before = getHostSibling(finishedWork);
      // 根据DOM兄弟节点是否存在决定调用parentNode.insertBefore或parentNode.appendChild执行DOM插入操作
      if (isContainer) {
        insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
      } else {
        insertOrAppendPlacementNode(finishedWork, before, parent);
      }
    }
    
  2. Update effect

    当Fiber节点含有Update effectTag,说明该Fiber节点需要更新。调用的方法为commitWork,他会根据Fiber.tag分别处理。

    function commitWork(current: Fiber | null, finishedWork: Fiber): void {
      if (!supportsMutation) {
        switch (finishedWork.tag) {
           //...
          case SimpleMemoComponent: {
           	commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
          }
         //...
        }
      }
         
      switch (finishedWork.tag) {
        //...
        case HostComponent: {
          //...
          commitUpdate(
                instance,
                updatePayload,
                type,
                oldProps,
                newProps,
                finishedWork,
              );
          }
          return;
        }
    }
    
  3. Deletion effect

    当Fiber节点含有Deletion effectTag,说明该Fiber节点对应的DOM节点需要从页面中删除。调用的方法为commitDeletion

    该方法会执行如下操作:

    1. 递归调用Fiber节点及其子孙Fiber节中fiber.tag为ClassComponent的componentWillUnmount生命周期钩子,从页面移除Fiber节对应DOM节点
    2. 解绑ref
    3. 调度useEffect的销毁函数
    function commitDeletion(
      finishedRoot: FiberRoot,
      current: Fiber,
      renderPriorityLevel: ReactPriorityLevel,
    ): void {
      if (supportsMutation) {
        unmountHostComponents(finishedRoot, current, renderPriorityLevel);
      } else {
        commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
      }
      const alternate = current.alternate;
      detachFiberMutation(current);
      if (alternate !== null) {
        detachFiberMutation(alternate);
      }
    }
    

commitLayoutEffects

该函数属于layout阶段,主要做的事:

  1. 调用commitLayoutEffectOnFiber执行相关生命周期函数或者hook相关callback
  2. 执行commitAttachRef为ref赋值
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;
    // 调用commitLayoutEffectOnFiber执行生命周期和hook
    if (effectTag & (Update | Callback)) {
      const current = nextEffect.alternate;
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    }
    // ref赋值
    if (effectTag & Ref) {
      commitAttachRef(nextEffect);
    }
    nextEffect = nextEffect.nextEffect;
  }
}

commitLayoutEffectOnFiber方法会根据fiber.tag对不同类型的节点分别处理:

  • 对于ClassComponent,他会通过current === null?区分是mount还是update,调componentDidMount或componentDidUpdate。
  • 对于FunctionComponent及相关类型,他会调用useLayoutEffect hook回调函数,调度useEffect销毁回调函数

在源码中commitLayoutEffectOnFiber函数的别名是commitLifeCycles,在简化后的代码中可以看到,commitLifeCycles会判断fiber的类型,SimpleMemoComponent会执行useLayoutEffect的回调,然后调度useEffect,ClassComponent会执行componentDidMount或者componentDidUpdate,this.setState第二个参数也会执行,HostRoot会执行ReactDOM.render函数的第三个参数,例如

ReactDOM.render(<App />, document.querySelector("#root"), function() {
  console.log("root mount");
});

现在可以知道useLayoutEffect是在commit阶段同步执行,useEffect会在commit阶段异步调度

function commitLifeCycles(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  switch (finishedWork.tag) {
    case SimpleMemoComponent: {
      // 此函数会调用useLayoutEffect的回调
      commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
      // 向pendingPassiveHookEffectsUnmount和pendingPassiveHookEffectsMount中push effect						// 并且调度它们
      schedulePassiveEffects(finishedWork);
    }
    case ClassComponent: {
      //条件判断...
      instance.componentDidMount();
      //条件判断...
      instance.componentDidUpdate(//update 在layout期间同步执行
        prevProps,
        prevState,
        instance.__reactInternalSnapshotBeforeUpdate,
      );      
    }
     
          
    case HostRoot: {
      commitUpdateQueue(finishedWork, updateQueue, instance);//render第三个参数
    }
         
  }
}

在schedulePassiveEffects中会将useEffect的销毁和回调函数push到pendingPassiveHookEffectsUnmount和pendingPassiveHookEffectsMount中

function schedulePassiveEffects(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 {
      const {next, tag} = effect;
      if (
        (tag & HookPassive) !== NoHookEffect &&
        (tag & HookHasEffect) !== NoHookEffect
      ) {
        //push useEffect的销毁函数并且加入调度
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
        //push useEffect的回调函数并且加入调度
        enqueuePendingPassiveHookEffectMount(finishedWork, effect);
      }
      effect = next;
    } while (effect !== firstEffect);
  }
}

commitAttachRef:

commitAttachRef中会判断ref的类型,执行ref或者给ref.current赋值

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;
     
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
     
    if (typeof ref === "function") {
      // 执行ref回调
      ref(instanceToUse);
    } else {
      // 如果是值的类型则赋值给ref.current
      ref.current = instanceToUse;
    }
  }
}

layout阶段

这个阶段主要做的事:

  1. 根据rootDoesHavePassiveEffects赋值相关变量
  2. 执行flushSyncCallbackQueue处理componentDidMount等生命周期或者useLayoutEffect等同步任务
// layout阶段
  const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
// 根据rootDoesHavePassiveEffects赋值相关变量
if (rootDoesHavePassiveEffects) {
  rootDoesHavePassiveEffects = false;
  rootWithPendingPassiveEffects = root;
  pendingPassiveEffectsLanes = lanes;
  pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {}
//...
     
// 在离开commitRoot函数前调用,触发一次新的调度,确保任何附加的任务被调度
ensureRootIsScheduled(root, now());
     
// ...

// 执行同步任务,这样同步任务不需要等到下次事件循环再执行
// 比如在 componentDidMount 中执行 setState 创建的更新会在这里被同步执行
// 或useLayoutEffect
flushSyncCallbackQueue();
     
return null;

current Fiber树切换

在结束本节的学习前,我们看下这行代码:

root.current = finishedWork;

workInProgress Fiber树commit阶段完成渲染后会变为current Fiber树。这行代码的作用就是切换fiberRootNode指向的current Fiber树。