React-render阶段——completeWork

648 阅读7分钟

概览

React-render阶段一解释了当组件进入reconciler后的执行过程, 从root节点开始调度, 循环调用beginWork创建子节点. 其中创建子节点的过程又分为挂载阶段和更新阶段, 挂载阶段不追踪副作用, 更新阶段追踪副作用, 更新阶段又分为可复用和不可复用, 可复用的会进入bailout的复用逻辑, 会把current树中的当前节点以及其子节点复制到workInProgress树中, 没有进入bailout阶段的Fiber节点会进入diff算法(对应的current中的Fiber节点与返回的JSX对比, 生成新的Fiber节点), 并为新的Fiber节点打上effectTag

当前Fiber节点没有子节点时就进入了completeWork, 可以理解为递归阶段的归阶段, completeWork的目的就是为了创建好对应的dom节点插入对应的父级节点的dom节点, 为其添加副作用标识, 再commit阶段将对应的节点展示到页面上并执行对应的副作用.

以下我以cra创建的项目的代码举例, js如下

function App() {
  const [num, setNum] = useState(0)
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p onClick={() => { setNum(num + 1) }}>
          {num} <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

CompleteWork执行时机与分析

completeWork发生在当前Fiber节点没有子节点的情况下, 源码发生在performUnitOfWork函数中, 这个函数发生在上文提到过的workLoopSync中, 这个函数将被循环调用

completeWork终止条件.png 上图打了两个断点处就是completeWork是否执行的条件, 第一个断点拿到的是beginWork创建好的子Fiber节点, 如果没有子Fiber节点则返回null, 只有当nextnull的时候才会进入completeWork, completeWork的开始源于他的上层函数completeUnitOfWork

function completeUnitOfWork(unitOfWork) {
  var completedWork = unitOfWork; // 获取当前completeWork的Fiber节点

  do {
    var current = completedWork.alternate; // 获取当前completeWork对应的current树上的节点, 没有则表示是新增的节点
    var returnFiber = completedWork.return; 

    if ((completedWork.flags & Incomplete) === NoFlags) {
      setCurrentFiber(completedWork);
      var next = void 0;

      if ( (completedWork.mode & ProfileMode) === NoMode) {
        next = completeWork(current, completedWork, subtreeRenderLanes);
      } else {
        startProfilerTimer(completedWork);
        next = completeWork(current, completedWork, subtreeRenderLanes); // Update render duration assuming we didn't error.

        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
      }

      resetCurrentFiber();

      if (next !== null) {
        workInProgress = next;
        return;
      }
    } else {
      var _next = unwindWork(completedWork, subtreeRenderLanes); // Because this fiber did not complete, don't reset its expiration time.


      if (_next !== null) {
        _next.flags &= HostEffectMask;
        workInProgress = _next;
        return;
      }

      if ( (completedWork.mode & ProfileMode) !== NoMode) {
        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); // Include the time spent working on failed children before continuing.

        var actualDuration = completedWork.actualDuration;
        var child = completedWork.child;

        while (child !== null) {
          actualDuration += child.actualDuration;
          child = child.sibling;
        }

        completedWork.actualDuration = actualDuration;
      }

      if (returnFiber !== null) {
        returnFiber.flags |= Incomplete;
        returnFiber.subtreeFlags = NoFlags;
        returnFiber.deletions = null;
      }
    }

    var 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


    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从源码中可以看到是一个do while循环, 终止条件有completeWork !== null或者循环内return前的几个终止条件, 我们可以看到有一个是siblingFiber不为null的情况. 即当前的节点存在兄弟节点时并且已经没有子节点, 当前节点会结束completeWork, 跳出调用栈, 执行下一次循环, 进入兄弟节点的beginWork.当兄弟节点为null的时候, 那么completeWork会被赋值为returnFiber, 这个时候注意并没有用return跳出调用栈, 因为父级节点的beginWork已经被执行, 因此会进入父级节点的completeWork, 由此向上, 当completeWorknull时意味着归到根节点

接下来分析一下completeWork做的具体的事情

function completeWork(current, workInProgress, renderLanes) {
  console.log('completeWork', 'tag:', workInProgress.tag, ' type:', workInProgress.type);
  var 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:
      bubbleProperties(workInProgress);
      return null;

    case ClassComponent:
      {
        var Component = workInProgress.type;

        if (isContextProvider(Component)) {
          popContext(workInProgress);
        }

        bubbleProperties(workInProgress);
        return null;
      }

    case HostComponent:
      {
        popHostContext(workInProgress);
        var rootContainerInstance = getRootHostContainer();
        var type = workInProgress.type;

        if (current !== null && workInProgress.stateNode != null) {
          updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);

          if (current.ref !== workInProgress.ref) {
            markRef$1(workInProgress);
          }
        } else {
          if (!newProps) {
            if (!(workInProgress.stateNode !== null)) {
              {
                throw Error( "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.


            bubbleProperties(workInProgress);
            return null;
          }

          var 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.

          var _wasHydrated = popHydrationState(workInProgress);

          if (_wasHydrated) {
            // TODO: Move this and createInstance step into the beginPhase
            // to consolidate.
            if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
              // If changes to the hydrated node need to be applied at the
              // commit-phase we mark this as such.
              markUpdate(workInProgress);
            }
          } else {
            var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
            appendAllChildren(instance, workInProgress, false, false);
            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.

            if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
              markUpdate(workInProgress);
            }
          }

          if (workInProgress.ref !== null) {
            // If there is a ref on a host node we need to schedule a callback
            markRef$1(workInProgress);
          }
        }

        bubbleProperties(workInProgress);
        return null;
      }
    // ...以下省略的是针对其他组件的执行逻辑, 这里我们重点关注前几个
}

上面的代码中看到completeWork函数就是针对不同的Fiber节点的Tag, 处理不同的逻辑, 我们根据p标签为例, 会进入caseHostComponent的分支

// completeWork顺序如下(展示一部分, 归到p标签为止)
1. img
2. {num}
3. num后面的空格
4. code
5.  and save to reload.
6. p

mount阶段

根据上面的completeWork的分析, 我们直接看上面 HostComponent 的逻辑, 一行一行看 开始的逻辑都是一样的, 首先处理context, 获取根容器

  1. popHostContext是和context相关的逻辑, 暂时跳过
  2. rootContainerInstance是获取根容器<div id="root"></div> 接下来的逻辑会根据挂载和更新进入不同的条件语句, 重新贴一下核心代码
if (current !== null && workInProgress.stateNode != null) {
    updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);

    if (current.ref !== workInProgress.ref) {
    markRef$1(workInProgress);
    }
} else {
    if (!newProps) { // 如果没有新的props并且stateNode为null, 可能是React发生了内部错误, 挂载时newProps至少也是一个{}, 一定不会进这里
        if (!(workInProgress.stateNode !== null)) {
            {
                throw Error( "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.
        bubbleProperties(workInProgress);
        return null;
    }

    var currentHostContext = getHostContext(); // context相关
    var _wasHydrated = popHydrationState(workInProgress); // 服务端渲染相关

    if (_wasHydrated) {// 服务端渲染
        if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
            
            markUpdate(workInProgress);
        }
    } else {
        var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
        appendAllChildren(instance, workInProgress, false, false);
        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.

            if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
                markUpdate(workInProgress);
            }
        }

        if (workInProgress.ref !== null) {
            // If there is a ref on a host node we need to schedule a callback
            markRef$1(workInProgress);
        }
}

跳过context相关和服务端渲染相关, 会进入 instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress), 这里主要是创建dom实例, 进去看下这个函数

function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
  var parentNamespace;

  {
    var hostContextDev = hostContext;
    // 检测dom是否正确嵌套
    validateDOMNesting(type, null, hostContextDev.ancestorInfo);

    if (typeof props.children === 'string' || typeof props.children === 'number') {
      var string = '' + props.children;
      var ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type);
      validateDOMNesting(null, string, ownAncestorInfo);
    }

    parentNamespace = hostContextDev.namespace;
  }

  // 创建dom实例
  var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
  // 缓存这个fiber节点
  precacheFiberNode(internalInstanceHandle, domElement);
  // 更新fiber节点的props, react会自己定义一个值, 所有的props将存放在当前的dom实例上
  updateFiberProps(domElement, props);
  return domElement;
}

返回创建好的domElement, 然后直接插入逻辑, 对应的代码为appendAllChildren(instance, workInProgress, false, false);, 看下这个函数

appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {
    // 拿到当前工作单元的child Fiber节点, 即拿到第一个子节点
    var node = workInProgress.child;

    while (node !== null) {
        if (node.tag === HostComponent || node.tag === HostText) { // 如果是文本节点或者是原生dom节点
            // 这个函数调用的就是parent.appendChild(node.stateNode);
            appendInitialChild(parent, node.stateNode);
        } else if (node.tag === HostPortal) ; else if (node.child !== null) {
            node.child.return = node;
            node = node.child;
            continue;
        }

        if (node === workInProgress) { // 如果是当前工作单元, 插入完毕
            return;
        }

        while (node.sibling === null) { // 没有兄弟节点则Fiber向上冒泡
            if (node.return === null || node.return === workInProgress) {
                return;
            }

            node = node.return;
        }

        node.sibling.return = node.return; // 把兄弟节点的return节点赋值给父节点
        node = node.sibling; // 把node赋值为兄弟节点
    }
};

appendAllChildren的作用和函数名相同, 目的就是把当前工作单元的所有子节点全部插入到刚创建好的dom实例中, 全部插入完毕执行workInProgress.stateNode = instance;, 这里采用的是深度优先遍历的方式 此时这里的instance为插入完的dom实例, 并把对应的节点赋值到当前Fiber节点的stateNode

插入完的dom实例.png

然后执行的是finalizeInitialChildren方法, 此方法调用了setInitialProperties

function setInitialProperties(domElement, tag, rawProps, rootContainerElement) {
  var isCustomComponentTag = isCustomComponent(tag, rawProps);

  {
    validatePropertiesInDevelopment(tag, rawProps);
  } // TODO: Make sure that we check isMounted before firing any of these events.


  var props;

  switch (tag) {
    // 跳过一些dom节点的判断逻辑
    default:
      props = rawProps;
  }

  // 判断props是否合法
  assertValidProps(tag, props);

  // 设置初始化的dom属性
  setInitialDOMProperties(tag, domElement, rootContainerElement, props, isCustomComponentTag);

  switch (tag) {
    case 'input':
      track(domElement);
      postMountWrapper(domElement, rawProps, false);
      break;

    case 'textarea':
      track(domElement);
      postMountWrapper$3(domElement);
      break;

    case 'option':
      postMountWrapper$1(domElement, rawProps);
      break;

    case 'select':
      postMountWrapper$2(domElement, rawProps);
      break;

    default:
      if (typeof props.onClick === 'function') {
        // TODO: This cast may not be sound for SVG, MathML or custom elements.
        trapClickOnNonInteractiveElement(domElement);
      }

      break;
  }
}

上面的函数主要是判断了props是否合法, 并对特殊的dom节点做了一些操作, 并把初始化的属性赋值到当前的dom

function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {
  for (var propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }

    var nextProp = nextProps[propKey];

    if (propKey === STYLE) { // style的时候
      {
        if (nextProp) {
          Object.freeze(nextProp);
        }
      }
      setValueForStyles(domElement, nextProp);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { // dangerouslySetInnerHTML
      var nextHtml = nextProp ? nextProp[HTML$1] : undefined;

      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    } else if (propKey === CHILDREN) { // children
      if (typeof nextProp === 'string') {  // 如果节点是字符串
        var 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) ; else if (propKey === AUTOFOCUS) ; else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        if ( typeof nextProp !== 'function') {
          warnForInvalidEventListener(propKey, nextProp);
        }

        if (propKey === 'onScroll') {
          listenToNonDelegatedEvent('scroll', domElement);
        }
      }
    } else if (nextProp != null) {
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

复制初始化的props是调用了setInitialDOMProperties, 这个函数循环调用了新的props, 并对每个propKey做了特定的赋值操作, 这一步主要在setValueForProperty中, 这一步会调用node.setAttribute来为创建好的dom元素设置属性

继续走下去进行的是判断是否存在ref, 如果存在ref则调用markRef$1(workInProgress);函数

最后执行bubbleProperties

if ( (completedWork.mode & ProfileMode) !== NoMode) {
      var actualDuration = completedWork.actualDuration;
      var treeBaseDuration = completedWork.selfBaseDuration;
      var child = completedWork.child;

      while (child !== null) {
        newChildLanes = mergeLanes(newChildLanes, mergeLanes(child.lanes, child.childLanes));
        subtreeFlags |= child.subtreeFlags;
        subtreeFlags |= child.flags;

        actualDuration += child.actualDuration;
        treeBaseDuration += child.treeBaseDuration;
        child = child.sibling;
      }

      completedWork.actualDuration = actualDuration;
      completedWork.treeBaseDuration = treeBaseDuration;

bubbleProperties 根据fiber.childfiber.child.sibling更新subtreeFlagschildLanes, 主要是为了标记子树有没有更新, 这样可以通过 fiber.subtreeFlags 快速判断子树是否有副作用钩子,不需要深度遍历. 在React17版本后使用subtreeFlags替换了finishWork.firstEffect的副作用链表, 操作主要发生在bubbleProperties函数中, 核心代码如下

update阶段

当进入update阶段, 假如我们把p节点的Fiber作为案例, 对应case为HostComponent会进入以下的条件分支

if (current !== null && workInProgress.stateNode != null) {
    updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);

    if (current.ref !== workInProgress.ref) {
        markRef$1(workInProgress);
    }
}

updateHostComponent$1代码如下

updateHostComponent$1 = function (current, workInProgress, type, newProps, rootContainerInstance) {
    var oldProps = current.memoizedProps; // 此状态为更新, 获取current的props

    if (oldProps === newProps) { // 判断props是否相同, 相同表示进入了bailout阶段,哪怕children变了我们也不需要做什么操作,因此直接跳过
      return;
    }

    var instance = workInProgress.stateNode; // 获取实例
    var currentHostContext = getHostContext();

    var updatePayload = prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext);

    workInProgress.updateQueue = updatePayload;

    if (updatePayload) {
      // 标记更新, 内部直接设置workInProgress.flags |= Update
      markUpdate(workInProgress);
    }
  };

这里调用了一个主要的更新方法为prepareUpdate, 返回的updatePayload将被加入工作单元的更新队列中, 这个函数调用了diffProperties, 其中返回的updatePayload是一个数组, 第i项是对应的propKey, 第i + 1项是对应的value, 当存在updatePayload的时候意味着这个HostComponent存在增,或者更新的情况, 会调用markUpdate进行更新

function diffProperties(domElement, tag, lastRawProps, nextRawProps, rootContainerElement) {
  {
    validatePropertiesInDevelopment(tag, nextRawProps);
  }

  var updatePayload = null;
  var lastProps;
  var nextProps;

  switch (tag) {
    // 这里省略了对特性的dom标签比如(input, select等)赋值lastProps和nextProps的过程
    default:
      lastProps = lastRawProps;
      nextProps = nextRawProps;

      if (typeof lastProps.onClick !== 'function' && typeof nextProps.onClick === 'function') {
        trapClickOnNonInteractiveElement(domElement);
      }

      break;
  }

  // 检验props
  assertValidProps(tag, nextProps);
  var propKey;
  var styleName;
  var styleUpdates = null;
  
  for (propKey in lastProps) {
    // 针对删除的情况, 需要标记对应的propKey为null
    if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {
      continue;
    }

    if (propKey === STYLE) {
      var lastStyle = lastProps[propKey];

      for (styleName in lastStyle) {
        if (lastStyle.hasOwnProperty(styleName)) {
          if (!styleUpdates) {
            styleUpdates = {};
          }

          styleUpdates[styleName] = '';
        }
      }
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) ; else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING) ; else if (propKey === AUTOFOCUS) ; else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (!updatePayload) {
        updatePayload = [];
      }
    } else {
      (updatePayload = updatePayload || []).push(propKey, null);
    }
  }

  for (propKey in nextProps) {
    var nextProp = nextProps[propKey];
    var lastProp = lastProps != null ? lastProps[propKey] : undefined;
    // 针对新增或者更新的情况, 需要标记对应的propKey为null
    if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) { // 对应props被删除的情况
      continue;
    }

    if (propKey === STYLE) {
      {
        if (nextProp) {
          Object.freeze(nextProp);
        }
      }

      if (lastProp) {
        // 在 `lastProp` 上取消设置样式,但不在 `nextProp` 上设置.
        for (styleName in lastProp) {
          if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
            if (!styleUpdates) {
              styleUpdates = {};
            }

            styleUpdates[styleName] = '';
          }
        } // 从lastProps中的style更新数据.


        for (styleName in nextProp) {
          if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
            if (!styleUpdates) {
              styleUpdates = {};
            }

            styleUpdates[styleName] = nextProp[styleName];
          }
        }
      } else {
        if (!styleUpdates) {
          if (!updatePayload) {
            updatePayload = [];
          }

          updatePayload.push(propKey, styleUpdates);
        }

        styleUpdates = nextProp;
      }
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      var nextHtml = nextProp ? nextProp[HTML$1] : undefined;
      var lastHtml = lastProp ? lastProp[HTML$1] : undefined;

      if (nextHtml != null) {
        if (lastHtml !== nextHtml) {
          (updatePayload = updatePayload || []).push(propKey, nextHtml);
        }
      }
    } else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string' || typeof nextProp === 'number') {
        (updatePayload = updatePayload || []).push(propKey, '' + nextProp);
      }
    } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING) ; else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        if ( typeof nextProp !== 'function') {
          warnForInvalidEventListener(propKey, nextProp);
        }

        if (propKey === 'onScroll') {
          listenToNonDelegatedEvent('scroll', domElement);
        }
      }

      if (!updatePayload && lastProp !== nextProp) {
        updatePayload = [];
      }
    } else if (typeof nextProp === 'object' && nextProp !== null && nextProp.$$typeof === REACT_OPAQUE_ID_TYPE) {
      nextProp.toString();
    } else {
      (updatePayload = updatePayload || []).push(propKey, nextProp);
    }
  }

  if (styleUpdates) {
    {
      validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
    }

    (updatePayload = updatePayload || []).push(STYLE, styleUpdates);
  }

  return updatePayload;
}

执行完这个diffProperties, 再执行bubbleProperties(workInProgress), 然后就结束了当前节点的completeWork

注意点

  1. Fibertagfunction的时候是不会进入completeWork
  2. 挂载的时候插入的dom节点的获取方式在于完成的finishedWork, 在performSyncWorkOnRoot函数中
  3. React17之前原本有一根finishWork.firstEffect开始的副作用链表, 始终指向的是第一个产生副作用的链表, 链表的nextEffect指向的是下一个具有副作用的链表, 这根链表在React17版本后使用subtreeFlags替换了, 操作主要发生在bubbleProperties函数中

全流程

这里梳理一下上述所说的全流程

React-render归阶段完整流程.png