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

  • React 源码解析系列 - React 的 render 阶段(一):根本流程介绍
  • React 源码解析系列 - React 的 render 阶段(二):beginWork
  • 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 次要做了两件事:执行 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 的状况;而在这种状况下,同样须要做“新建”操作。

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/appendChildexport 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 beginWork7. span Fiber beginWork8. span Fiber completeWork // 以后节点 —— <span />, appendChild 文本节点9. p Fiber beginWork10. p Fiber completeWork  // 以后节点 —— <p />, appendChild 文本节点11. Fragment Fiber completeWork // 跳过12. div Fiber completeWork // 上面咱们来重点介绍这一块13. App Fiber completeWork14. 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) 来开启 React commit 阶段的工作。