React 源码之 "归" 阶段 mount 时的流程

373 阅读4分钟

这是专栏里的文章,专栏里每一篇文章都是接上一篇文章的,是一个非常细的分析过程。

上一篇文章,我们知道render阶段采用深度优先遍历的方式依次执行 beginWork 和 completeWork

completeWork 函数

本篇文章来看看归阶段mount时的流程,也就是 completeWork 所做的工作。

第一次进入

此时已经是归阶段了,第一次进入是img节点,current是null。 image.png 然后会根据不同的tag进入不同的case判断 image.png

img节点是 HostComponent 所以会进入这个case image.png

case HostComponent:
  {
    popHostContext(workInProgress);
    var rootContainerInstance = getRootHostContainer();
    var type = workInProgress.type;
    // 判断current是否存在,首屏渲染的时候是不存在的,所以首屏不会进入updateHostComponent更新函数
    if (current !== null && workInProgress.stateNode != null) {
      updateHostComponent(current, workInProgress, type, newProps, rootContainerInstance);

      if (current.ref !== workInProgress.ref) {
        markRef(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." );
          }
        }
        bubbleProperties(workInProgress);
        return null;
      }

      var currentHostContext = getHostContext();
      // popHydrationState是跟ssr相关的
      var _wasHydrated = popHydrationState(workInProgress);

      if (_wasHydrated) {
        if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
          markUpdate(workInProgress);
        }
      } else {
        // 创建对应的DOM节点
        var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
        appendAllChildren(instance, workInProgress, false, false);
        workInProgress.stateNode = instance;
        if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
          markUpdate(workInProgress);
        }
      }

      if (workInProgress.ref !== null) {
        markRef(workInProgress);
      }
    }

    bubbleProperties(workInProgress);
    return null;
  }

创建对应的DOM节点

createInstance 函数是用来创建对应的DOM节点的函数。 image.png

createInstance 函数内部:

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

  {
    var hostContextDev = hostContext;
    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;
  }

  var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  return domElement;
}

上面的代码是 createInstance 源码,内部 createElement 函数执行完 domElement 就是 img fiber 节点对应的是 DOM 元素。

当创建完DOM节点之后,就会将DOM节点插入到已经创建好的DOM树中,appendAllChildren 函数就是进行插入操作的。

image.png

接下来是将 DOM 节点保存到 workInProgress.stateNode 属性上。有了DOM节点之后,就会为这个DOM节点添加属性:alt src等。

设置属性的操作都是在 finalizeInitialChildren 中执行的:

image.png

finalizeInitialChildren 函数,首先会判断这是否是一个自定义的标签,接下来会根据 host component 的 type 来进入不同的逻辑。

function finalizeInitialChildren(domElement, type, props, rootContainerInstance, hostContext) {
  setInitialProperties(domElement, type, props, rootContainerInstance);
  return shouldAutoFocusHostComponent(type, props);
}

image.png

setInitialProperties 函数里,接下来会判断我们的 props 是否合法,然后进入初始化DOM属性的操作。

image.png

setInitialDOMProperties 执行完之后,最终:setAttribute 方法会为我们的属性赋值。当执行完这个操作之后,一个Fiber节点的 completeWork 就完成了。

下一个 fiber 节点的 completeWork 是一个文本节点:

image.png

根绝判断进入如下代码:HostText 位置,对文本节点进行处理。

case HostText:
  {
    var newText = newProps;
    if (current && workInProgress.stateNode != null) {
      var oldText = current.memoizedProps;
      updateHostText(current, workInProgress, oldText, newText);
    } else {
      if (typeof newText !== 'string') {
        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." );
          }
        }
      }
      // 校验各种
      var _rootContainerInstance = getRootHostContainer();
      var _currentHostContext = getHostContext();
      var _wasHydrated2 = popHydrationState(workInProgress);
      if (_wasHydrated2) {
        if (prepareToHydrateHostTextInstance(workInProgress)) {
          markUpdate(workInProgress);
        }
      } else {
        // 得到text的DOM节点,加入到:workInProgress.stateNode
        workInProgress.stateNode = createTextInstance(newText, _rootContainerInstance, _currentHostContext, workInProgress);
      }
    }
    bubbleProperties(workInProgress);
    return null;
  }

最后也是得到text的DOM节点,加入到:workInProgress.stateNode。 image.png

appendAllChildren 插入DOM节点

当前的 working progress 是 code 节点。

image.png

appendAllChildren 每次执行它时,都会将已经创建好的 DOM 节点挂载在当前的 DOM 节点下。

当我们归阶段从每一个子节点向上归到根节点时,创建的每一个子节点的 DOM 元素都会挂载在它的父级 DOM 节点下。

当我们执行到 App 时,我们已经有一颗构建好的 DOM 树。

在 reconcelChildren 中,当某个节点不存在对应的 current 节点时,就不会被标记 Effect tag, 那么首屏渲染这些 DOM 节点是如何挂载在页面中呢?

重新刷新页面,可以看到对于首屏渲染会有一个节点进入到这个逻辑

image.png

reconcileSingleElement执行完成之后,会进入到 placeSingleChild 进行对 Fiber 节点处理,newFiber.flags 加入 Placement

image.png

最后会生成一颗 Fiber 树。每个 Fiber 节点 child 就是子节点。

总结

mount时归的过程是将节点生成一颗完整的Fiber树,而主要进行归操作的函数就是:completeWork。

mount时的主要进行函数:beginWork & completeWork。

beginWork 目的:为了查找子元素 completeWork 目的:达到最后的子元素往回查找,然后继续执行 beginWork

在执行 completeWork 时,会进行对已经生成好的DOM节点进行插入操作,慢慢的生成一颗完整的Fiber树。