8
头图

Series article directory (synchronized update)

This series of articles discusses the source code of React v17.0.0-alpha

performUnitOfWork

Recall the performUnitOfWork method introduced in "React Source Code Analysis Series - React's Render Stage (1): Basic Process Introduction" :

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节点
  }

  // ...省略
}

As the "return" phase of render, it will be executed after the "delivery" phase of render; in other words, when beginWork returns a null value, that is, when the current node no (or does not need to pay attention to) the child Fiber node of the current node , will enter the "return" stage of render - completeUnitOfWork .

completeUnitOfWork

Let's look at the protagonist of this article - completeUnitOfWork method:

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);

  // 省略...
}

Please see the flowchart:

react源码解析 - completeUnitOfWork流程图

It can be seen from the flow chart that completeUnitOfWork mainly does two things: execute completeWork and close EffectList , the following two details will be introduced.

completeWork

If the beginWork method of the "delivery" stage is mainly to create child nodes, then the completeWork method of the "return" stage is mainly to create the DOM node of the current node, and to close the DOM node and EffectList of the child node.
Similar to beginWork , completeWork will also perform different logic according to the different types of tag of the current node:

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;
    }
  // ...省略
}

It should be noted that many types of nodes do not have the logic of completeWork (that is, return null without any operation), such as the very common Fragment and FunctionComponent . We focus on the HostComponent necessary for page rendering, which is the Fiber node converted from the HTML tag (eg <div></div> ).

Handling 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;
    }
    // ...省略
  }
}

From the above code snippet, we can know that the completeWork method mainly has two code branches for the processing of HostComponent:

  • (current !== null && workInProgress.stateNode != null) === true , do " update " operation to the current node;
  • (current !== null && workInProgress.stateNode != null) === true , do " New " operation on the current node;

The reason why the commonly used mount (first screen rendering) and update in the previous article is not used here is because there is a situation, it is current !== null and workInProgress.stateNode === null : at the time of update, if the current Fiber node is a new node, it is already in The beginWork stage is marked with Placement effectTag, then there will be a situation where the stateNode is null; and in this case, it is also necessary to do the " new " operation.

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

"Update" action for HostComponent

In this code branch, since it has been judged that workInProgress.stateNode !== null , that is, the corresponding DOM node already exists, there is no need to generate a DOM node.

We can see that this block mainly executes a updateHostComponent method:

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);
  }
};

It can be seen from the above code snippet that the main function of updateHostComponent is to calculate the DOM node attributes that need to be changed, and add the Update EffectTag to the current node.

prepareUpdate

Next, let's see how the prepareUpdate methods calculate the DOM node attributes that need to be changed:

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,
  );
}

It can be seen that prepareUpdate actually calls the diffProperties method directly.

diffProperties

diffProperties method has a lot of code, I will not release the source code here, I will briefly talk about the process:

  1. Do special processing for lastProps & nextProps of a specific tag (because this scenario is dealing with HostComponent, so tag is the html tag name), including input/select/textarea, for example: the value of input may be a number, while the value of native input is only Accepts string, so you need to convert the data type here.
  2. Traverse lastProps:

    1. If the prop also exists in nextProps, it will be skipped, which is equivalent to the prop has not changed and need not be processed.
    2. When you see a prop with style, it is sorted into the styleUpdates variable (object), and this part of the style attribute is set to an empty value
    3. Push the propKey except the above cases into an array (updatePayload), and push a null value into the array to clear the prop.
  3. Traverse nextProps:

    1. If the nextProp is the same as lastProp, that is, there is no change before and after the update, skip it.
    2. When you see a prop with style, organize it into the styleUpdates variable. Note that this part of the style attribute has value.
    3. Handling DANGEROUSLY_SET_INNER_HTML
    4. handle children
    5. In addition to the above scenarios, directly push the key and value of the prop into the array (updatePayload).
  4. If styleUpdates is not empty, then push both the 'style' and styleUpdates variables into the array (updatePayload).
  5. Return updatePayload.

updatePayload is an array where the element of the array even index is prop key and the element of the array base index is prop value .

markUpdate

Next, let's look at the markUpdate method. The method is actually very simple, that is, hit workInProgress.effectTag on Update EffectTag .

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

"New" action for HostComponent

The main logic of the "New" operation includes three:

  • Generate the corresponding DOM node for the Fiber node: createInstance method
  • Insert descendant DOM nodes into newly generated DOM nodes: appendAllChildren method
  • Initialize all properties of the current DOM node and event callback processing: finalizeInitialChildren method
createInstance

Let's look at the method of " generates the corresponding DOM node for the Fiber node" - 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;
}

It can be seen that createInstance mainly calls the createElement method to create DOM elements; as for createElement, this article does not expand, if you are interested, you can look at the source code of .

appendAllChildren

Let's look at the method of "insert descendant DOM nodes into newly generated DOM nodes" - 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 is essentially a depth-first traversal with a conditional restriction (limiting the progressive level):

  1. Take the first child of the current node (parent) as the loop body ( node ).
  2. If the loop body is a html tag or a Fiber node corresponding to plain text, then give its DOM appendChild to parent .
  3. If the current loop body ( node ) has a sibling node ( node.sibling ), set that sibling node as the body of the next loop.

Just looking at the above process, isn't this a typical breadth-first traversal? Don't worry, because there is a special case: when the current loop body is not the Fiber node corresponding to the html tag or plain text, and the current loop body has child nodes ( node.child ), the child node of the current loop body is used as the next time. The body of the loop and immediately start the next loop ( continue ).

Take the following component as an example:

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

According to the execution order of beginWork and completeWork in "React Source Code Analysis Series - React's Render Stage (1): Basic Process Introduction" , we can draw:

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

Let's focus on appendAllChildren in the div node:

  1. Initialization before the while loop is executed: take out the first child node of the div node, the b node, as the main body of the first while loop.
  2. The first while loop (the body of the loop is the b node):

    1. The b node is a HostComponent that appendChild directly.
    2. The b node has a sibling, the Fragment node, which is set as the body of the next while loop ( node ).
  3. The second while loop (the body of the loop is the Fragment node):

    1. Since the Fragment node is neither a HostComponent nor a HostText, the first child of the Fragment node, the span node, will be taken as the body of the next while loop ( node ).
    2. Immediately enter ( continue ) the next while loop.
  4. The third while loop (the body of the loop is the span node):

    1. The span node is a HostComponent that appendChild directly.
    2. The span node has a sibling, the p node, which is set as the body of the next while loop ( node ).
  5. The fourth while loop (the body of the loop is the p node):

    1. The p node is a HostComponent that appendChild directly.
    2. The p node has no sibling nodes, so the regression ( node = node.return ) is performed. At this time, in the "regression" code segment - a small while loop, the loop body becomes the parent node of the p node, that is, the Fragment node.
    3. Continue to the next small while loop: Since Fragment has no sibling nodes, it does not meet the end condition of the small while loop, so continue to "regress", at this time the main body of the loop ( node ) is the div node.
    4. Continue to the next small while loop: Since the div node satisfies node.return === workInProgress , the entire traversal process is directly ended - appendAllChildren.
finalizeInitialChildren

Let's look at "initialize all properties of the current DOM node and event callback processing" - finalizeInitialChildren :

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

From the above code snippet, we can clearly see that finalizeInitialChildren is mainly divided into two steps:

  1. Execute the setInitialProperties method; note that this method is different from prepareUpdate, this method will actually mount the DOM properties to the DOM node, and will also actually call addEventListener to bind the event processing callback to the current DOM node of.
  2. Execute shouldAutoFocusHostComponent method: return the value of props.autoFocus (only button / input / select / textarea supported).

Collapse EffectList

As the basis for DOM operation, the commit stage needs to find all Fiber nodes with effectTag and execute the corresponding effectTag operations in sequence. Do we need to traverse the Fiber tree again in the commit stage? This is obviously inefficient.

In order to solve this problem, in completeUnitOfWork, each Fiber node that has completed Work and has effectTag will be stored in a singly linked list called effectList; the first Fiber node in effectList is stored in fiber.firstEffect, the last one Elements are stored in fiber.lastEffect.

Similar to appendAllChildren, in the "return" phase, all Fiber nodes with effectTag will be appended to the effectList of the parent node, and finally form a singly linked list with rootFiber.firstEffect as the starting point.

If the current Fiber node (completedWork) also has an EffectTag, then put it (in the EffectList) after the child Fiber nodes.

/* 如果父节点的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 ends

There are two ending scenarios for completeUnitOfWork:

  • The current node ( completed ) has a sibling node ( completed.sibling ), at this time, workInProgress (that is, the loop body of performUnitOfWork) will be set as the sibling node, and then the completeUnitOfWork method will be terminated, and then the next performUnitOfWork will be performed, in other words: execute the " The "pass" phase of the "sibling node" - beginWork .
  • In the process of "regression" of completeUnitOfWork, the value of completed is null , that is, the regression of the entire Fiber tree has been completed; at this time, the value of workInProgress is null, which means that the while loop in the workLoopSync / workLoopConcurrent method has also reached the end Condition; at this point, React's render phase ends.

When the render phase, in performSyncWorkOnRoot method, calls commitRoot(root) (here root mass participation refers fiberRootNode) to open React the commit session.


array_huang
10.4k 声望6.6k 粉丝