React 源码解析系列 - React 的 render 阶段(三):completeUnitOfWork

系列文章目录(同步更新)

本系列文章均为讨论 React v17.0.0-alpha 的源码

performUnitOfWork

回忆《React 源码解析系列 - React 的 render 阶段(一):基本流程介绍》中介绍的 performUnitOfWork 方法:

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate; // current树上对应的Fiber节点,有可能为null
  // ...省略

  let next; // 用来存放beginWork()返回的结果
  next = beginWork(current, unitOfWork, subtreeRenderLanes);

  // ...省略
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) { // beginWork返回null,表示无(或无需关注)当前节点的子Fiber节点
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next; // 下次的workLoopSync/workLoopConcurrent的while循环的循环主体为子Fiber节点
  }

  // ...省略
}
复制代码

作为 render 的“归”阶段,需在 render 的“递”阶段结束后才会执行;换句话说,当 beginWork 返回 null 值,即当前节点无(或无需关注)当前节点的子Fiber节点时,才会进入到 render 的“归”阶段 —— completeUnitOfWork

completeUnitOfWork

下面来看本文的主角 —— completeUnitOfWork 方法:

function completeUnitOfWork(unitOfWork: Fiber): void {
  /*
	完成对当前Fiber节点的一些处理
    处理完成后,若当前节点尚有sibling节点,则结束当前方法,进入到下一次的performUnitOfWork的循环中
    若已没有sibling节点,则回退处理父节点(completedWork.return),
    直到父节点为null,表示整棵 workInProgress fiber 树已处理完毕。
   */
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    if ((completedWork.effectTag & Incomplete) === NoEffect) {
      let next;
      // ...省略
	  next = completeWork(current, completedWork, subtreeRenderLanes);
      // ...省略
	  
      /*
		假如completeWork返回不为空,则进入到下一次的performUnitOfWork循环中
		但这种情况太罕见,目前我只看到Suspense相关会有返回,因此此代码段姑且认为不会执行
	   */
      if (next !== null) {
        workInProgress = next;
        return;
      }

      // ...省略

      if (
        returnFiber !== null &&
        (returnFiber.effectTag & Incomplete) === NoEffect
      ) {
		/* 收集所有带有EffectTag的子Fiber节点,以链表(EffectList)的形式挂载在当前节点上 */
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }
        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }
          returnFiber.lastEffect = completedWork.lastEffect;
        }

		/* 如果当前Fiber节点(completedWork)也有EffectTag,那么将其放在(EffectList中)子Fiber节点后面 */
        const effectTag = completedWork.effectTag;
		/* 跳过NoWork/PerformedWork这两种EffectTag的节点,NoWork就不用解释了,PerformedWork是给DevTools用的 */
        if (effectTag > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }
          returnFiber.lastEffect = completedWork;
        }
      }
    } else {
      // 异常处理,省略...
    }

	// 取当前Fiber节点(completedWork)的兄弟(sibling)节点;
    // 如果有值,则结束completeUnitOfWork,并将该兄弟节点作为下次performUnitOfWork的主体(unitOfWork)
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    // 若没有兄弟节点,则将在下次do...while循环中处理父节点(completedWork.return)
    completedWork = returnFiber;
	// 此处需要注意!
	// 虽然把workInProgress置为completedWork,但由于没有return,即没有结束completeUnitOfWork,因此没有意义
	// 直到completedWork(此时实际上是本循环中原completedWork.return)为null,结束do...while循环后
    // 此时completeUnitOfWork的运行结果(workInProgress)为null
    // 也意味着performSyncWorkOnRoot/performConcurrentWorkOnRoot中的while循环也达到了结束条件
    workInProgress = completedWork;
  } while (completedWork !== null);

  // 省略...
}
复制代码

请看流程图:

completeUnitOfWork流程图.jpg

由流程图可知, completeUnitOfWork 主要做了两件事:执行 completeWork收拢 EffectList ,下面详细介绍一下这两块内容。

completeWork

如果说“递”阶段的 beginWork 方法主要是创建子节点,那么“归”阶段的 completeWork 方法则主要是创建当前节点的 DOM 节点,并对子节点的 DOM 节点和 EffectList 进行收拢。 类似 beginWork , completeWork 也会根据当前节点不同的 tag 类型执行不同的逻辑:

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略
}
复制代码

需要注意的是,很多类型的节点是没有 completeWork 这一块的逻辑的(即啥操作都没做就直接 return null),比如非常常见的 FragmentFunctionComponent 。我们重点关注页面渲染所必须的 HostComponent ,即由 HTML 标签(如 <div></div>)转换成的 Fiber 节点。

处理 HostComponent

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
	// ...省略
    case HostComponent: {
      // ...省略
      const type = workInProgress.type;
      if (current !== null && workInProgress.stateNode != null) {
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );

        // ...省略
      } else {
        // ...省略
        const instance = createInstance(
          type,
          newProps,
          rootContainerInstance,
          currentHostContext,
          workInProgress,
        );

        appendAllChildren(instance, workInProgress, false, false);
        workInProgress.stateNode = instance;

        if (
          finalizeInitialChildren(
            instance,
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
          )
        ) {
          markUpdate(workInProgress);
        }
      }
      return null;
	}
	// ...省略
  }
}
复制代码

从上面这个代码段我们可以得知, completeWork 方法对 HostComponent 的处理主要有两个代码分支:

  • (current !== null && workInProgress.stateNode != null) === true 时,对当前节点做“更新”操作;
  • (current !== null && workInProgress.stateNode != null) === true 时,对当前节点做“新建”操作;

这里之所以没有用之前文章里常用的 mount(首屏渲染) 和 update 来表达,是因为存在一种情况,是 current !== nullworkInProgress.stateNode === null 的:在 update 时,假如当前的 Fiber 节点是个新的节点,已经在 beginWork 阶段被打上了 Placement effectTag ,那么就会存在 stateNode 为 null 的情况;而在这种情况下,同样需要做“新建”操作。

completeWork(处理 HostComponent 代码段)流程图.jpg

HostComponent 的“更新”操作

在此代码分支中,由于已经判断 workInProgress.stateNode !== null,即已存在对应的 DOM 节点,所以不需要再生成 DOM 节点。

我们可以看到这块主要是执行了一个 updateHostComponent 方法:

updateHostComponent = function(
  current: Fiber,
  workInProgress: Fiber,
  type: Type,
  newProps: Props,
  rootContainerInstance: Container,
) {
  /* 假如props没有变化(当前节点是通过bailoutOnAlreadyFinishedWork方法来复用的),可以跳过对当前节点的处理 */
  const oldProps = current.memoizedProps;
  if (oldProps === newProps) {
    return;
  }

  const instance: Instance = workInProgress.stateNode;
  // 省略...
  /* 计算需要变化的DOM节点属性,以数组方式存储(数组偶数索引的元素为属性名,数组基数索引的元素为属性值) */
  const updatePayload = prepareUpdate(
    instance,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
    currentHostContext,
  );
  // 将计算出来的updatePayload挂载在workInProgress.updateQueue上,供后续commit阶段使用
  workInProgress.updateQueue = (updatePayload: any); 
  // 如果updatePayload不为空,则给当前节点打上Update的EffectTag
  if (updatePayload) {
    markUpdate(workInProgress);
  }
};
复制代码

从上面的代码段可以看出 updateHostComponent 的主要作用就是计算出需要变化的 DOM 节点属性,并给当前节点打上Update的EffectTag。

prepareUpdate

接下来我们看 prepareUpdate 方法是如何计算出需要变化的 DOM 节点属性的:

export function prepareUpdate(
  domElement: Instance,
  type: string,
  oldProps: Props,
  newProps: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): null | Array<mixed> {
  // 省略DEV代码...
  return diffProperties(
    domElement,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
  );
}
复制代码

可以看出 prepareUpdate 其实是直接调用了 diffProperties 方法。

diffProperties

diffProperties 方法的代码比较多,我这边就不放源码了,大概讲一下过程:

  1. 对特定 tag (由于本场景是处理 HostComponent ,因此 tag 即 html 标签名)的 lastProps & nextProps 做特殊处理,包括 input/select/textarea ,举例:input 的 value 值可能会是个 number ,而原生 input 的 value 只接受 string,因此这里需要转换数据类型。
  2. 遍历 lastProps:
    1. 如果该 prop 在 nextProps 中也存在,那么就跳过,相当于该 prop 没有变化,无需处理。
    2. 见到有 style 的 prop 就整理到 styleUpdates 变量(object)中,这部分 style 属性被置为空值
    3. 把除以上情况外的 propKey 推进一个数组(updatePayload)中,另外再推一个 null 值进数组中,表示把该 prop 清空掉。
  3. 遍历 nextProps:
    1. 如果该 nextProp 与 lastProp 一致,即更新前后没有发生变化,则跳过。
    2. 见到有 style 的 prop 就整理到 styleUpdates 变量中,注意这部分 style 属性是有值的
    3. 处理 DANGEROUSLY_SET_INNER_HTML
    4. 处理 children
    5. 除以上场景外,直接把 prop 的 key 和值都推进数组(updatePayload)中。
  4. 如果 styleUpdates 不为空,那么就把'style'和 styleUpdates 变量都推进数组(updatePayload)中。
  5. 返回 updatePayload。

updatePayload 是个数组,其中数组偶数索引的元素为 prop key ,数组基数索引的元素为 prop value

markUpdate

接着来看 markUpdate 方法,该方法其实很简单,就是在 workInProgress.effectTag 上打了个 Update EffectTag

function markUpdate(workInProgress: Fiber) {
  // Tag the fiber with an update effect. This turns a Placement into
  // a PlacementAndUpdate.
  workInProgress.effectTag |= Update;
}
复制代码

HostComponent 的“新建”操作

“新建”操作的主要逻辑包括三个:

  • 为 Fiber 节点生成对应的 DOM 节点: createInstance 方法
  • 将子孙 DOM 节点插入刚生成的 DOM 节点中: appendAllChildren 方法
  • 初始化当前 DOM 节点的所有属性以及事件回调处理: finalizeInitialChildren 方法
createInstance

下面来看“为 Fiber 节点生成对应的 DOM 节点”的方法 —— createInstance

export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  let parentNamespace: string;
  // 省略DEV代码段...
  // 确定该DOM节点的命名空间(xmlns属性),一般是"http://www.w3.org/1999/xhtml"
  parentNamespace = ((hostContext: any): HostContextProd); 
  // 创建 DOM 元素
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  // 在 DOM 对象上创建指向 fiber 节点对象的属性(指针),方便后续取用
  precacheFiberNode(internalInstanceHandle, domElement);
  // 在 DOM 对象上创建指向 props 的属性(指针),方便后续取用
  updateFiberProps(domElement, props);
  return domElement;
}
复制代码

可以看出 createInstance 主要是调用了 createElement 方法来创建 DOM 元素;至于 createElement 本文不展开,有兴趣可以看看源码

appendAllChildren

下面来看“将子孙 DOM 节点插入刚生成的 DOM 节点中”的方法 —— appendAllChildren :

// completeWork是这样调用的:appendAllChildren(instance, workInProgress, false, false);

appendAllChildren = function(
  parent: Instance, // 相对于要append的子节点来说,completeWork当前处理的节点就是父节点
  workInProgress: Fiber,
  needsVisibilityToggle: boolean,
  isHidden: boolean,
) {
  let node = workInProgress.child; // 第一个子Fiber节点
  /* 这个while循环本质上是一个深度优先遍历 */
  while (node !== null) {
    if (node.tag === HostComponent || node.tag === HostText) {
      // 如果是html标签或纯文本对应的子节点,则将当前子节点的DOM添加到父节点的DOM子节点列表末尾
      appendInitialChild(parent, node.stateNode);
    } else if (enableFundamentalAPI && node.tag === FundamentalComponent) { // 先忽略
      appendInitialChild(parent, node.stateNode.instance);
    } else if (node.tag === HostPortal) {
	  // ...无操作
    } else if (node.child !== null) {
	  // 针对一些特殊类型的子节点,如<Fragment />,尝试从子节点的子节点获取DOM
      node.child.return = node; // 设置好return指针,方便后续辨别是否达到循环结束条件
      node = node.child; // 循环主体由子节点变为子节点的子节点
      continue; // 立即开展新一轮的循环
    }
    if (node === workInProgress) {
      return; // 遍历“回归时”发现已经达到遍历的结束条件,结束遍历
    }
	// 若当前循环主体node已无兄弟节点(sibling),则进行“回归”;且如果“回归”一次后发现还是没有sibling,将继续“回归”
    while (node.sibling === null) {
      if (node.return === null || node.return === workInProgress) {
        return; // “回归”过程中达到遍历的结束条件,结束遍历
      }
      node = node.return; // “回归”的结果:将node.return作为下次循环的主体
    }
    // 走到这里就表明当前循环主体有sibling
    node.sibling.return = node.return; // 设置好return指针,方便后续辨别是否达到循环结束条件
    node = node.sibling; // 将node.sibling作为下次循环的主体
  }
};

// appendInitialChild本质上就是执行了appendChild这个原生的DOM节点方法
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
export function appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void {
  parentInstance.appendChild(child);
}
复制代码

appendAllChildren 本质上是一个有条件限制(限制递进层次)的深度优先遍历:

  1. 取出当前节点(parent)的第一个子节点作为循环主体(node)。
  2. 如果该循环主体是 html 标签或纯文本对应的 Fiber 节点,则将其 DOM appendChildparent
  3. 如果当前循环主体(node)有兄弟节点(node.sibling),则将该兄弟节点设为下次循环的主体。

光看上面这个流程,这不是一个典型的广度优先遍历吗?别急,因为还有一种比较特殊的情况:当当前循环主体不是 html 标签或纯文本对应的 Fiber 节点,且当前循环主体有子节点(node.child)时,将当前循环主体的子节点作为下次循环的主体,并立即开始下次循环(continue)。

以下面这个组件作为例子:

function App() {
    return (
        <div>
            <b>1</b>
			<Fragment>
            	<span>2</span>
				<p>3</p>
			</Fragment>
        </div>
    )
}
复制代码

根据《React 源码解析系列 - React 的 render 阶段(一):基本流程介绍》里对 beginWork 和 completeWork 的执行顺序可以得出:

1. rootFiber beginWork 
2. App Fiber beginWork 
3. div Fiber beginWork 
4. b Fiber beginWork 
5. b Fiber completeWork // 当前节点 —— <b />, appendChild 文本节点
6. Fragment Fiber beginWork
7. span Fiber beginWork
8. span Fiber completeWork // 当前节点 —— <span />, appendChild 文本节点
9. p Fiber beginWork
10. p Fiber completeWork  // 当前节点 —— <p />, appendChild 文本节点
11. Fragment Fiber completeWork // 跳过
12. div Fiber completeWork // 下面我们来重点介绍这一块
13. App Fiber completeWork
14. rootFiber completeWork
复制代码

我们来重点介绍 div 节点中的 appendAllChildren

  1. while 循环执行前初始化:取出 div 节点的第一个子节点 —— b 节点,作为第一次 while 循环的主体。
  2. 第一次 while 循环(循环主体为 b 节点):
    1. b 节点是一个 HostComponent ,直接 appendChild 。
    2. b 节点有一个兄弟节点,即 Fragment 节点,将其设置为下一次 while 循环的主体(node)。
  3. 第二次 while 循环(循环主体为 Fragment 节点):
    1. 由于 Fragment 节点既不是 HostComponent 也不是 HostText ,因此将取 Fragment 节点的第一个子节点 —— span 节点作为下次 while 循环的主体(node)。
    2. 立即进入(continue)下一次 while 循环。
  4. 第三次 while 循环(循环主体为 span 节点):
    1. span 节点是一个 HostComponent ,直接 appendChild 。
    2. span 节点有一个兄弟节点,即 p 节点,将其设置为下一次 while 循环的主体(node)。
  5. 第四次 while 循环(循环主体为 p 节点):
    1. p 节点是一个 HostComponent ,直接 appendChild 。
    2. p 节点没有兄弟节点,进行回归(node = node.return),此时在该“回归”代码段 —— 一个小 while 循环中,循环主体变为 p 节点的父节点,即 Fragment 节点。
    3. 继续下一次小 while 循环:由于 Fragment 也没有兄弟节点,不满足小 while 循环的结束条件,因此继续进行“回归”,此时循环主体(node)为 div 节点。
    4. 继续下一次小 while 循环:由于 div 节点满足node.return === workInProgress,因此直接结束整个遍历过程 —— appendAllChildren。
finalizeInitialChildren

下面来看“初始化当前 DOM 节点的所有属性以及事件回调处理” —— finalizeInitialChildren

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

从上面的代码段,我们可以很清晰地看到 finalizeInitialChildren 主要分为两个步骤:

  1. 执行 setInitialProperties 方法;注意,该方法与 prepareUpdate 不一样,该方法是会真正将 DOM 属性挂载到 DOM 节点上的,也会真正地调用 addEventListener 把事件处理回调绑定在当前 DOM 节点上的。
  2. 执行 shouldAutoFocusHostComponent 方法:返回 props.autoFocus 的值(仅 button / input / select / textarea 支持)。

收拢 EffectList

作为 DOM 操作的依据,commit 阶段需要找到所有带有 effectTag 的 Fiber 节点并依次执行effectTag 对应操作,难道还需要在 commit 阶段再遍历一次 Fiber 树吗?这显然是很低效的。

为了解决这个问题,在 completeUnitOfWork 中,每个执行完 completeWork 且存在 effectTag 的 Fiber 节点会被保存在一条被称为 effectList 的单向链表中; effectList 中第一个 Fiber 节点保存在 fiber.firstEffect ,最后一个元素保存在 fiber.lastEffect 。

类似 appendAllChildren ,在“归”阶段,所有有 effectTag 的 Fiber 节点都会被追加在父节点的 effectList 中,最终形成一条以 rootFiber.firstEffect 为起点的单向链表。

如果当前 Fiber 节点(completedWork)也有 EffectTag ,那么将其放在( EffectList 中)子 Fiber 节点的后面。

/* 如果父节点的effectList头指针为空,那么就直接把本节点的effectList头指针赋给父节点的头指针,相当于把本节点的整个effectList直接挂在父节点中 */
if (returnFiber.firstEffect === null) {
    returnFiber.firstEffect = completedWork.firstEffect;
}
/* 如果父节点的effectList不为空,那么就把本节点的effectList挂载在父节点effectList的后面 */
if (completedWork.lastEffect !== null) {
    if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
    }
    returnFiber.lastEffect = completedWork.lastEffect;
}

/* 如果当前Fiber节点(completedWork)也有EffectTag,那么将其放在(EffectList中)子Fiber节点后面 */
const effectTag = completedWork.effectTag;
/* 跳过NoWork/PerformedWork这两种EffectTag的节点,NoWork就不用解释了,PerformedWork是给DevTools用的 */
if (effectTag > PerformedWork) {
  if (returnFiber.lastEffect !== null) {
     returnFiber.lastEffect.nextEffect = completedWork;
  } else {
     returnFiber.firstEffect = completedWork;
  }
     returnFiber.lastEffect = completedWork;
  }
}
复制代码

completeUnitOfWork 结束

completeUnitOfWork 有两种结束的场景:

  • 当前节点(completed)有兄弟节点(completed.sibling),此时会将 workInProgress(即 performUnitOfWork 的循环主体)设为该兄弟节点,然后结束掉 completeUnitOfWork 方法,此后将进行下一次 performUnitOfWork ,换句话说:执行该“兄弟节点”的“递”阶段 —— beginWork 。
  • 在 completeUnitOfWork “回归”的过程中, completed 的值为 null ,即当前已完成整棵 Fiber 树的回归;此时, workInProgress 的值为 null ,这意味着 workLoopSync / workLoopConcurrent 方法中的 while 循环也到达了结束条件;至此, React 的 render 阶段结束。

当 render 阶段结束时,在 performSyncWorkOnRoot 方法中,会调用 commitRoot(root) (这里的 root 传参指的是 fiberRootNode )来开启 React commit 阶段的工作。

猜你喜欢

转载自juejin.im/post/7017703203525361695