React源码06 - 完成节点任务

06 - 完成节点任务

完成节点更新之后完成节点的创建,并提供优化到最小幅度的DOM更新列表。

1. completeUnitOfWork

第 04 篇说过 renderRoot 做的事情:

  • 调用 workLoop 进行循环单元更新
  • 捕获错误并进行处理
  • 走完流程之后善后

现在在 workLoop 中调用 performUnitOfWork。
next = beginWork(current, workInProgress, nextRenderExpirationTime)
每次都 return child,以便继续循环向下单路查找,直到触及叶子节点(树枝到头了就是叶子)返回 null,也就是叶子节点没有 child 了。这算是 renderRoot 中完成了一次 workLoop 调用。
此时,传入该叶子节点执行 completeUnitOfWork。
之后尝试继续循环执行 wokrLoop,处理其他路。

completeUnitOfWork:

    1. 根据是否中断来调用不同的处理方法

在外部 renderRoot 使用 do...while 调用 workLoop 时会使用 try...catch,只要不是致命错误,就记录相应错误(比如 Suspense 的 promise 在中间过程中的合理报错),然后继续执行循环。

    1. 判断是否有兄弟节点来执行不同的操作
    1. 完成节点之后赋值 effect 链

在之前的 beginWork 中为节点标记了相应的 sideEffect,也就是等到 commit 阶段中更新 dom 时的操作依据(增删改等)。而在 completeUnitOfWork 中则将 fiber 上的 sideEffect 进一步进行串联,方便 commit 时使用。
之前第 03 篇中说过每个 fiber 上都有:

  • effectTag: SideEffectTag。用来记录 SideEffect。
  • nextEffect: Fiber | null。单链表用来快速查找下一个side effect。
  • firstEffect: Fiber | null。 子树中第一个side effect。
  • lastEffect: Fiber | null。子树中最后一个side effect。

有些像是层层嵌套的文件夹 A/B/C/D,B中只记录了C/D。这些打了 effectTag 标记的 fiber 节点通过这些指针单独组成单向链表,反正都是些指针引用,也不占多少空间。

通过不断地 completeUnitOfWork 将 effect 汇总串联到上层节点,最终 RootFiber 上的 firstEffect 到 lastEffect 这个链表中记录了所有带有 effectTag 的 fiber 节点,即最终在 commit 阶段所有需要应用到 dom 节点上的 SideEffect。

TODO:commitRoot 方法
**
SideEffectTag 清单:

export type SideEffectTag = number;

// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /*              */ 0b00000000000;
export const PerformedWork = /*         */ 0b00000000001;

// You can change the rest (and add more).
export const Placement = /*             */ 0b00000000010;
export const Update = /*                */ 0b00000000100;
export const PlacementAndUpdate = /*    */ 0b00000000110;
export const Deletion = /*              */ 0b00000001000;
export const ContentReset = /*          */ 0b00000010000;
export const Callback = /*              */ 0b00000100000;
export const DidCapture = /*            */ 0b00001000000;
export const Ref = /*                   */ 0b00010000000;
export const Snapshot = /*              */ 0b00100000000;

// Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /*   */ 0b00110100100;

// Union of all host effects
export const HostEffectMask = /*        */ 0b00111111111;

export const Incomplete = /*            */ 0b01000000000;
export const ShouldCapture = /*         */ 0b10000000000;

performUnitOfWork:

function performUnitOfWork(workInProgress: Fiber): Fiber | null {
  // The current, flushed, state of this fiber is the alternate.
  // Ideally nothing should rely on this, but relying on it here
  // means that we don't need an additional field on the work in
  // progress.
  const current = workInProgress.alternate;

  // See if beginning this work spawns more work.
  startWorkTimer(workInProgress);

  let next;
  if (enableProfilerTimer) {
    if (workInProgress.mode & ProfileMode) {
      startProfilerTimer(workInProgress);
    }

    next = beginWork(current, workInPrognextRenderExpirationTimeress, );
    workInProgress.memoizedProps = workInProgress.pendingProps;

    if (workInProgress.mode & ProfileMode) {
      // Record the render duration assuming we didn't bailout (or error).
      stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
    }
  } else {
    next = beginWork(current, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;
  }

  if (next === null) {
    // 如果这次遍历时调用beginWork返回了null,说明已经到了单路的叶子节点了,于是调用completeUnitOfWork。
    // 当前的workInProgress就是叶子节点,因为寻找child却返回了null,说明到头了。
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(workInProgress);
  }

  ReactCurrentOwner.current = null;

  return next;
}

completeUnitOfWork:
如果说 workLoop 在局部子树中是从上向下处理节点。那么 completeUnitOfWork 中则是在局部子树中从下向上处理节点。

if (next === null) {
  // 如果这次遍历时调用beginWork返回了null,说明已经到了单路的叶子节点了,于是调用completeUnitOfWork。
  // 当前的workInProgress就是叶子节点,因为寻找child却返回了null,说明到头了。
  // If this doesn't spawn new work, complete the current work.
  next = completeUnitOfWork(workInProgress);
}

completeUnitOfWork 代码有些多,主体是个 while 循环,其中有下面这个遍历逻辑:

  • 在 performUnitOfWork 中,如果从上至下单路遍历到了叶子节点,则开始调用 completeUnitOfWork 进行向上遍历。
  • 如果有 sibling 兄弟节点则 return 兄弟节点,以便 workLoop 中再次调用 performUnitOfWork 对刚才的兄弟节点进行遍历。
  • 又一次单路到头了,遇到了叶子节点,则再次 completeUnitOfWork 处理叶子节点。
  • 如果当前子树兄弟节点全处理完了,则向上对父节点进行 completeUnitOfWork 处理。如果父节点也有兄弟节点,则同理。

最终效果就是一整棵 RootFiber 树:

  • 每个节点都会先使用 performUnitOfWork 处理一次。
  • 再使用 completeUnitOfWork 处理一次。

**
completeUnitOfWork 节选:

function completeUnitOfWork(workInProgress: Fiber): Fiber | null
// ...
if (siblingFiber !== null) {
  // If there is more work to do in this returnFiber, do that next.
  return siblingFiber;
} else if (returnFiber !== null) {
  // If there's no more work in this returnFiber. Complete the returnFiber.
  workInProgress = returnFiber;
  continue;
} else {
  // 说明更新过程完成了,到rootFiber了。等着commit阶段了
  return null;
}

completeUnitOfWork 中的一些工作:

  • 重设 ChildExpirationTime
  • completeWork

2. 重设 ChildExpirationTime

ChildExpirationTime 记录了一个 fiber 的子树中优先级最高的更新时间。尽管产生更新需求的节点可能是整个应用 fiber 树中的某个节点,但进行更新调度时是从顶部 RootFiber 开始参与调度的。
因此通过 ChildExpirationTime 不断向上汇总子树的最高优先级的更新时间,最终 RootFiber 的 ChildExpirationTime 记录了整棵树中最高优先级的更新时间。
而在 completeUnitOfWork 从下向上进行信息汇总时,如果某个节点的更新任务已经得到执行,也就是没有自身的 expirationTime 了,那么 completeUnitOfWork 中就需要顺便不断向上重置 ChildExpirationTime。通过调用:
resetChildExpirationTime(workInProgress, nextRenderExpirationTime);

3. completeWork

  • pop 各种 context 相关的内容
  • 对于 HostComponent 执行初始化
  • 初始化监听事件

大部分类型的 fiber 节点不需要在这一步做什么事情(Suspense 类型以后再说),而以下两种类型有点东西:

  • HostComponent
  • HostText

下面主要谈谈 HostComponent(该 fiber 类型对应到原生 dom)在 completeWork 中的相关操作。

4. HostComponent

HostComponent 中涉及到 updateHostComponent。在一次更新而非渲染中:

  • diffProperties 计算需要更新的内容
  • 不同的 dom property 处理方式不同

首次渲染时

创建对应的 dom 实例:

let instance = createInstance(
  type,
  newProps,
  rootContainerInstance,
  currentHostContext,
  workInProgress,
);

appendAllChildren(instance, workInProgress, false, false);

if (
  finalizeInitialChildren( // finalizeInitialChildren 最终返回是否需要 auto focus 自动聚焦
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
  )
) {
  markUpdate(workInProgress); // 如果是需要 autoFocus,那么还要设置 sideEffect。
}
workInProgress.stateNode = instance;
function markUpdate(workInProgress: Fiber) {
  // Tag the fiber with an update effect. This turns a Placement into
  // a PlacementAndUpdate.
  workInProgress.effectTag |= Update;
}

function markRef(workInProgress: Fiber) {
  workInProgress.effectTag |= Ref;
}

appendAllChildren:
找到子树中第一层 dom 类型的节点,append 到自身对应的 dom 实例下。即 workInProgress.stateNode 所记录的原生 dom 实例。
因为在每次 completeUnitOfWork 时遇到 HostComponent 即原生 dom 对应的 fiber 类型时,都会 appendAllChildren。
所以也就是对 fiber 树这个虚拟 dom 进行提纯,最终从下向上构建出纯 dom 树。
**
finalizeInitialChildren:
appendAllChildren 之后紧接着 finalizeInitialChildren 初始化事件监听体系。
setInitialProperties 初始化事件监听(后面会单独讲事件监听)。
初始化 dom attribute(主要是一些原生 dom 操作)。
初始化markUpdate、mark*、 defaultValue、isControlled、处理 style 属性,px 补全等。

下面展开叙述。

finalizeInitialChildren 最终返回是否需要 auto focus 自动聚焦:

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  setInitialProperties(domElement, type, props, rootContainerInstance);
  return shouldAutoFocusHostComponent(type, props);
}

function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
  switch (type) {
    case 'button':
    case 'input':
    case 'select':
    case 'textarea':
      return !!props.autoFocus;
  }
  return false;
}

// ...
setInitialDOMProperties(
  tag,
  domElement,
  rootContainerElement,
  props,
  isCustomComponentTag,
);

setInitialDOMProperties:

function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      // 设置 style 属性
      // Relies on `updateStylesByID` not mutating `styleUpdates`.
      CSSPropertyOperations.setValueForStyles(domElement, nextProp);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      const nextHtml = nextProp ? nextProp[HTML] : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    } else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string') {
        // Avoid setting initial textContent when the text is empty. In IE11 setting
        // textContent on a