Skip to content

completeUnitOfWork

这个阶段的主要特点是:

  1. 产生 stateNode (当前节点对应的DOM,此时只是保存在内存中)
  2. 标记 flags 和 subtreeFlags (在Commit阶段作为判断当前分支是否有DOM的操作)

栗子

仍然是beginWork的那个栗子

image

初次渲染

在初次渲染的时候,我们根据beginWork按照深度遍历优先的流程知道其当performUnitOfWork执行到 第一个没有child的节点(this is Comp21的Text节点),因为next为null进行第一个 completeWork的流程

代码如下

js
function completeUnitOfWork(unitOfWork: Fiber): void {
  // Attempt to complete the current unit of work, then move to the next
  // sibling. If there are no more siblings, return to the parent fiber.
  let completedWork = unitOfWork;
  do {
    // 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.
    // 当前Node的原FiberNode实例对象
    const current = completedWork.alternate;
    // 父节点
    const returnFiber = completedWork.return;

    // Check if the work completed or if something threw.
    if ((completedWork.flags & Incomplete) === NoFlags) {
      let next;
      if (
        !enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode
      ) {
        next = completeWork(current, completedWork, subtreeRenderLanes);
      } else {
        next = completeWork(current, completedWork, subtreeRenderLanes);  
      }

      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        workInProgress = next;
        return;
      }
    } 
    // 如果存在兄弟节点 那么将兄弟节点赋给workInProgress 进入到 兄弟节点的 workLoop 的流程
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    // 如果 不存在兄弟节点 那么通过 do{} 循环执行 父节点的 completeWork流程
    //  此时 completedWork指向了父节点,那么在父节点完成 completeWork流程 时也会判断其是否存在兄弟节点,这样形成一个深度优先的链表 遍历流程
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while (completedWork !== null);

  // We've reached the root.
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

completeUnitOfWork(unitOfWork: Fiber)

不考虑completeWork的情况,可以看出来 completeUnitOfWork的主要作用是 配合 beginWork 完成

  1. 有兄弟节点的情况下,将workInProgress 指向兄弟节点
  2. 没有兄弟节点的情况下,执行父节点的 completeWork

image

beginWork(current, completedWork, subtreeRenderLanes)

image

bubbleProperties()

在React17的时候 我们发现多了一个 bubbleProperties()方法,其作用是通过subtreeFlags替代之前的 finishWork.firstEffect 副作用链表,来标记当前子树中存在副作用,从而避免在 commit的时候再次深度遍历寻找

  1. 在FiberNode树上通过FiberNode.subtreeFlags , FiberNode.flags 标记节点和子孙节点 DOM操作 类型

    1. FiberNode.flags 标记当前节点DOM的操作 更新、创建、删除等
    2. FiberNode.subtreeFlags 通过位运算标记当前节点树下子孙包含的操作类型

注意: 其跟 FiberNode.lanes 和 FiberNode.childLanes 很像

  1. 更新 FiberNode.childLanes

    合并当前节点的 lanes 和 childLanes (completedWork.childLanes = mergeLanes(newChildLanes, mergeLanes(child.lanes, child.childLanes)))

    在performUnitOfWork的时候,Update的FiberNode标记的是lanes ,祖先节点 标记点childLanes。

    那么这时候通过lanes 和 childLanes的合并,从而更好的根据优先级找到更新的节点树分支

js
/**
 * 1. 在FiberNode树上通过FiberNode.subtreeFlags , FiberNode.flags 标记节点和子孙节点 DOM操作 类型
 *   - FiberNode.flags 标记当前节点DOM的操作  更新、创建、删除等
 *   - FiberNode.subtreeFlags 通过位运算标记当前节点树下子孙包含的操作类型
 *  注意: 其跟 FiberNode.lanes 和 FiberNode.childLanes 很像
 *  2. 更新 FiberNode.childLanes
 *    合并当前节点的 lanes 和 childLanes (completedWork.childLanes = mergeLanes(newChildLanes, mergeLanes(child.lanes, child.childLanes)))
 *    在performUnitOfWork的时候,Update的FiberNode标记的是lanes ,祖先节点 标记点childLanes。
 *    那么这时候通过lanes 和 childLanes的合并,从而更好的根据优先级找到更新的节点树分支
 * @param {*} completedWork
 * @returns
 */
function bubbleProperties(completedWork: Fiber) {
  // 是否能够复用
  const didBailout =
    completedWork.alternate !== null &&
    completedWork.alternate.child === completedWork.child;

  let newChildLanes = NoLanes;
  let subtreeFlags = NoFlags;
  if (!didBailout) {
    // Bubble up the earliest expiration time.
    if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
    } else {
      let child = completedWork.child;
      while (child !== null) {
        // 合并当前节点的 lanes 和 childLanes
        newChildLanes = mergeLanes(
          newChildLanes,
          mergeLanes(child.lanes, child.childLanes),
        );
        // 跟 FiberNode.lanes 和 FiberNode.childLanes 很像
        // 通过位运算标记当前节点树下子孙包含的操作类型
        subtreeFlags |= child.subtreeFlags;
        subtreeFlags |= child.flags;

        child = child.sibling;
      }
    }
    // 通过位运算标记当前节点树下子孙包含的操作类型
    completedWork.subtreeFlags |= subtreeFlags;
  }
  completedWork.childLanes = newChildLanes;
  return didBailout;
}

createInstance()

根据FiberNode生成新的DOM元素,并在DOM上缓存 FiberNode 和 Props信息。

其还有一个差不多的 createTextInstance。差不多的意思。

主要作用就是:

  1. 根据环境的不同通过 rootDom.ownDocument去获取创建各种DOM的方法,并生成对应的DOM节点。(此时没有生成DOM节点的childens)

  2. 在DOM元素上缓存了两个属性

    • '__reactFiber$' + randomKey 缓存FiberNode 对象
    • '__reactProps$' + randomKey 缓存 Props 内容

image

js
/**
 * 根据FiberNode生成新的DOM元素,并在DOM上缓存 FiberNode 和 Props信息
 * @param {*} type DOM元素的类型
 * @param {*} props props
 * @param {*} rootContainerInstance  FiberRootNode的current 根DOM
 * @param {*} hostContext 跟rootContainerInstance获取上下文环境 
 * @param {*} internalInstanceHandle FiberNode对象
 * @returns
 */
export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  let parentNamespace: string;
  if (__DEV__) {
    // TODO: take namespace into account when validating.
    const hostContextDev = ((hostContext: any): HostContextDev);
    validateDOMNesting(type, null, hostContextDev.ancestorInfo);
    if (
      typeof props.children === 'string' ||
      typeof props.children === 'number'
    ) {
      const string = '' + props.children;
      const ownAncestorInfo = updatedAncestorInfo(
        hostContextDev.ancestorInfo,
        type,
      );
      validateDOMNesting(null, string, ownAncestorInfo);
    }
    parentNamespace = hostContextDev.namespace;
  } else {
    // 获取 parent的 namespace 命名空间
    parentNamespace = ((hostContext: any): HostContextProd);
  }
  // 创建 DOM 元素
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  // 在 DOM 元素上缓存 FiberNode 对象
  precacheFiberNode(internalInstanceHandle, domElement);
  //  在 DOM 元素上缓存 Props 对象
  updateFiberProps(domElement, props);
  return domElement;
}

appendAllChildren(instance, workInProgress, false, false);

在通过createInstance、createTextInstance等创建好当前FiberNode对应的DOM元素的时候,然后通过 appendAllChildren将其子节点DOM元素插入到当前DOM元素上,然后在全部插入完毕后,将整个DOM元素保存到 workInProgress.stateNode上。

下面看源码

finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext)

在上面通过 appendAllChildren 处理完DOM元素的子元素后,现在通过finalizeInitialChildren将FiberNode上的props赋值到DOM元素 attributes上。

下面看源码

js

/**
 * 1. 将 Prop的内容更新到 DOM元素 上
 * 2. 对于 autoFocus 其需要标记 flags 为 Update ,在commit单独处理
 *
 * @param {*} domElement
 * @param {*} type
 * @param {*} props
 * @param {*} rootContainerInstance
 * @param {*} hostContext
 * @returns
 */
export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  // 将 Prop的 内容更新到 DOM元素 上
  setInitialProperties(domElement, type, props, rootContainerInstance);
  // 是否有 autoFocus 属性 需要单独处理
  // 因为 autoFocus 不像其他的事件不会产生副作用
  //   对于 autoFocus 其需要标记 flags 为 Update ,在commit 进行处理
  return shouldAutoFocusHostComponent(type, props);
}

其本身还是很简单的,主要是进行了两个小步骤

  1. 将 Prop的 内容更新到 DOM元素 上(style、children)

  2. 对于 autoFocus 其需要标记 flags 为 Update ,在commit单独处理

    js
    /**
     * autoFocus 属性会 产生副作用
     * 当返回true的时候 会标记 flags为Update,在Commit阶段作为有更新的DOM元素处理 
     * @param {*} type
     * @param {*} props
     * @returns
     */
    function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
      switch (type) {
        case 'button':
        case 'input':
        case 'select':
        case 'textarea':
          return !!props.autoFocus;
      }
      return false;
    }

下面主要看一下

setInitialProperties(domElement, type, props, rootContainerInstance);

其主要功能是处理DOM元素的属性,并按照类型分别处理

  • style 属性

    setValueForStyles(domElement, nextProp); 进行处理

    • 空值进行过滤 "" null boolean
    • 单位类型的值 数字进行添加 px 单位
    • float 转换为 cssFloat
  • dangerouslySetInnerHTML

  • children 属性

  • autoFocus

    和其他事件不一起处理,而是单独拿出来,为什么?

    因为autoFocus这个属性会参数副作用,需要在渲染完成后聚焦

  • 其他 初始化注册的 事件 onClick

    详情看 合成事件

  • 其他

js
/**
 * 减Props的内容更新到 DOM元素 上
 *    此处主要处理以下:
 *   - style 属性
 *   - dangerouslySetInnerHTML
 *   - children 属性
 *   - autoFocus
 *   - 其他 初始化注册的 事件 onClick  -- 通过 ensureListeningTo 绑定事件
 * @param {*} tag
 * @param {*} domElement
 * @param {*} rootContainerElement
 * @param {*} nextProps
 * @param {*} isCustomComponentTag
 */
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];
    // style 属性的更新  <div style={{ fontSize : "20px" }}></div>
    if (propKey === STYLE) {
      // Relies on `updateStylesByID` not mutating `styleUpdates`.
      setValueForStyles(domElement, nextProp);
    //  dangerouslySetInnerHTML 属性
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      const nextHtml = nextProp ? nextProp[HTML] : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    // children
    } else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string') {
        // Avoid setting initial textContent when the text is empty. In IE11 setting
        // textContent on a <textarea> will cause the placeholder to not
        // show within the <textarea> until it has been focused and blurred again.
        // https://github.com/facebook/react/issues/6731#issuecomment-254874553
        const canSetTextContent = tag !== 'textarea' || nextProp !== '';
        if (canSetTextContent) {
          setTextContent(domElement, nextProp);
        }
      } else if (typeof nextProp === 'number') {
        setTextContent(domElement, '' + nextProp);
      }
    } else if (
      propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
      propKey === SUPPRESS_HYDRATION_WARNING
    ) {
    // Noop
    // autoFocus 属性
    } else if (propKey === AUTOFOCUS) {
      // We polyfill it separately on the client during commit.
      // We could have excluded it in the property list instead of
      // adding a special case here, but then it wouldn't be emitted
      // on server rendering (but we *do* want to emit it in SSR).
    // 事件类型的属性 处理
    } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        if (!enableEagerRootListeners) {
          ensureListeningTo(rootContainerElement, propKey, domElement);
        } else if (propKey === 'onScroll') {
          listenToNonDelegatedEvent('scroll', domElement);
        }
      }
    } else if (nextProp != null) {
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

下面我们开始具体将每一种组件类型的处理过程

HostComponent

js
 case HostComponent: {
      popHostContext(workInProgress);
      // 获取 Root 的 Container (div#root)
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      // current 和 stateNode 都不为空  说明是更新流程
      if (current !== null && workInProgress.stateNode != null) {
        // 更新 DOM
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );
        // 标记是否存在 ref 属性的 更新
        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      } else {
        const currentHostContext = getHostContext();
        // TODO: Move createInstance to beginWork and keep it on a context
        // "stack" as the parent. Then append children as we go in beginWork
        // or completeWork depending on whether we want to add them top->down or
        // bottom->up. Top->down is faster in IE11.
        // 服务端渲染有关
        const wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
          // 服务端渲染
        } else {
          // 生成 DOM 元素
          const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
          // 将子元素的DOM添加到当前DOM元素下
          appendAllChildren(instance, workInProgress, false, false);

          // 缓存DOM元素
          workInProgress.stateNode = instance;

          // Certain renderers require commit-time effects for initial mount.
          // (eg DOM renderer supports auto-focus for certain elements).
          // Make sure such renderers get scheduled for later work.
          // 处理DOM元素的属性,如果遇到autoFocus 并标记副作用
          if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            // 标记 flags  Update
            markUpdate(workInProgress);
          }
        }

        if (workInProgress.ref !== null) {
          // If there is a ref on a host node we need to schedule a callback
          // 标记 flags  Ref
          markRef(workInProgress);
        }
      }
      return null;
    }

分为更新流程 和 创建流程(SSR)

  1. 更新流程(current !== null && workInProgress.stateNode != null )

    • updateHostComponent

      其核心是一个 prepareUpdate(instance,type ,oldProps, newProps, rootContainerInstance , currentHostContext ),并将需要更新的信息存在 workInProgress.updateQueue中

      markUpdate(workInProgress); 标记有更新

    • ref 更新的标识 markRef(workInProgress);

HostText

文本类型的FiberNode 的 completeWork

js
case HostText: {
      const newText = newProps;
      if (current && workInProgress.stateNode != null) {
        const oldText = current.memoizedProps;
        // If we have an alternate, that means this is an update and we need
        // to schedule a side-effect to do the updates.
        updateHostText(current, workInProgress, oldText, newText);
      } else {
        if (typeof newText !== 'string') {
          invariant(
            workInProgress.stateNode !== null,
            'We must have new props for new mounts. This error is likely ' +
              'caused by a bug in React. Please file an issue.',
          );
          // This can happen when we abort work.
        }
        const rootContainerInstance = getRootHostContainer();
        const currentHostContext = getHostContext();
        const wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
          if (prepareToHydrateHostTextInstance(workInProgress)) {
            markUpdate(workInProgress);
          }
        } else {
          workInProgress.stateNode = createTextInstance(
            newText,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
        }
      }
      bubbleProperties(workInProgress);
      return null;
    }

副作用标记和副作用链

React17

在旧的任务调度中,如果在 completeWork阶段发生DOM的操作(更新、添加、删除等),都会在FiberNode.flags上标记。

同时触发

js
      /**
       * 在根节点上按照深度优先的规则形成一个
       *    firstEffect指向第一个有副作用的DOM
       *    lastEffect指向最后一个有副作用的DOM
       *    中间更新的DOM 通过 firstEffect.nextEffect.xxxxxx 形成一个单链
       * 因为 CompleteWork是一个由下而上的过程,所以通过
       *  将当前节点completedWork.firstEffect保存到父节点的 lastEffect.nextEffect(单链)。
       *  然后一层层往上传播,直到 returnFiber !== null 即遇到根节点截止。
       *  这样就可以在根节点上收集到所有的产生副作用的节点 FiberNode
       *
       */
      if (
        returnFiber !== null &&
        // Do not append effects to parents if a sibling failed to complete
        (returnFiber.flags & Incomplete) === NoFlags
      ) {
        // Append all the effects of the subtree and this fiber onto the effect
        // list of the parent. The completion order of the children affects the
        // side-effect order.
        // 如果父节点的 firstEffect 为null 所以暂时没有,所以当前作为第一个
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }
        // 如果当前节点的lastEffect有值
        //   1. 如果父节点的 lastEffect 有值,那么就将当前Node变成 lastEffect.nextEffect 父节点的 lastEffect变成本身
        //   2. 如果没有值,那么直接将父节点的lastEffect 变成本身
        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }
          returnFiber.lastEffect = completedWork.lastEffect;
        }

        // If this fiber had side-effects, we append it AFTER the children's
        // side-effects. We can perform certain side-effects earlier if needed,
        // by doing multiple passes over the effect list. We don't want to
        // schedule our own side-effect on our own list because if end up
        // reusing children we'll schedule this effect onto itself since we're
        // at the end.
        const flags = completedWork.flags;

        // Skip both NoWork and PerformedWork tags when creating the effect
        // list. PerformedWork effect is read by React DevTools but shouldn't be
        // committed.
        // 如果当前节点产生DOM的操作
        if (flags > PerformedWork) {
          // 将当前节点加入到父节点.firstEffect
          // 1. 如果当前节点同一层级的前面节点没有副作用,那么这时候 父节点.firstEffect == 父节点.lastEffect == null
          //      将当前节点加入到 父节点.firstEffect == 父节点.lastEffect == completedWork
          // 2. 如果当前节点同一层级的前面节点也有副作用,那么其不修改父节点..lastEffect.nextEffect = completedWork ; 父节点.lastEffect = completedWork;
          //      这样父节点的 fistEffect.nextEffect.nextEffect  === lastEffect
          //
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }
          returnFiber.lastEffect = completedWork;
        }

image

这样在触发点击事件的时候在跟FiberNode.alternate上就可以看到一个

image

React18

在新版的任务调度中 对于副作用的标记主要通过 flags subtreeFlags 去标记副作用节点分支,舍弃了之前的根节点通过副作用firstEffect、lastEffect、nextEffect 形成的单链结构,去找到当前需要更新的DOM元素的FiberNode节点。

而是使用 flags subtreeFlags 位运算的操作去深度遍历标记