React源码阅读系列——从ReactDOM.render开始

1,831 阅读5分钟

说明

从本篇文章开始,会进行React源码阅读并总结成文章进行分享。由于React源码部分的内容比较多,代码量也非常大,所以要分几大模块去进行阅读和分析,再结合所有模块进行一个整体的总结,这样才能对React整体代码的实现逻辑和内部思想有一个更深入的理解。

具体React部分,目前只分析与web相关的部分(因为React还有React-Native部分的代码也在源码里面,这里不涉及)。通过React源码的目录结构可以看到里面的内容非常多:

2020-03-08_171725

本系列文章暂时只关心核心部分的实现代码,有以下几个大的模块:

2020-03-08_171851

  • react

这部分主要是React对象的核心顶层API的实现代码,例如:React.Component,React.Children,React.useState,React.createElement,React.cloneElement等常用的方法和类。

  • react-dom

这部分代码定义了ReactDOM对象的所有API,这部分可以理解是React和DOM之间的粘合剂部分,这部分的代码实现了所有对DOM的操作的封装,React部分的改变最终会使用这个部分的代码去更新DOM视图的内容。这里面就包括最常用的ReactDOM.render方法,这也是本文后面会详细介绍的内容,React的所有源码部分的阅读就从这个入口开始。同时react-dom也实现了所有合成事件以及相关的事件绑定、事件分发等内容。同时这部分还有服务端(server)渲染部分的代码,不过这里暂时不涉及到这部分的代码。

  • react-reconciler

这是目前react部分算法核心以及最复杂的代码部分。它叫做调和器(reconciler),这里包含了Fiber(React使用的一系列渲染树的算法)、Schedule(调度)。React之所以具有跨平台的特性,也跟这里的调和器有关,正是因为有了React-Reconciler,它只关心组件元素之间的关系和如何更新,而不涉及具体的渲染实现,所以可以做到提供一套统一的算法,去输出给不同的端去实现最终的视图渲染(web端和natvie端,甚至其他的渲染方式)。同时这部分还涉及到整个React.Component的生命周期的事件的调用。

  • events

说到DOM,肯定离不开事件,React实现了一套事件系统,实现了合成事件,还有整体的事件监听和分发调度。后续会专门分享事件系统部分的源码实现。

  • shared

shared包下,存放的是各种可以共享的类型数据以及一些方法和默认的变量参数。这部分的代码很多模块都会用到。所以在阅读整个源码的过程中也会经常看到这个部分的代码。

从ReactDOM.render开始

每一个使用React的开发人员都会用到的一个方法就是ReactDOM.render(),这也是一个React App的入口代码。我们可以先通过打印ReactDOM,来看看都有哪些方法:

import ReactDOM from 'react-dom';
console.log('ReactDOM');
console.log(ReactDOM);

打印如下图所示:

2020-03-08_173130

可以看到很多常用的方法,其中render是再熟悉不过的,下面看看render方法都做了什么事情

整个render方法的调用流程,如下图所示:

ReactDOM render()

下面开始一步步地进行剖析:

首先我们开发React页面的时候,入口处的代码如下所示:

import React from 'react';
import ReactDOM from 'react-dom';
import App from 'app';

ReactDOM.render(<App/>, document.getElementById('app'));

接下来看看ReactDOM的render方法实现的源码(摘取部分源码,省略了其他不重要的部分,方便阅读):

const ReactDOM: Object = {
  createPortal,

  findDOMNode(
    componentOrElement: Element | ?React$Component<any, any>,
  ): null | Element | Text {...},

  hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {...},

  render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },
};

可以看到render方法传入了三个参数,分别是element,container,callback。element是传入的React元素类型,例如通过React.createElement方法创建的React元素;container是真实的DOM节点,一般都是app的挂载根节点;callback是render方法执行完成后的回调函数。callback非必传参数,而且一般比较少用。render方法内部调用并返回了legacyRenderSubtreeIntoContainer方法。

接下来看legacyRenderSubtreeIntoContainer方法的源码实现(同样省略了部分无关代码):

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  forceHydrate: boolean,
  callback: ?Function,
) {
  let root: Root = (container._reactRootContainer: any);
  if (!root) {
    // 初始化挂在,root为空的情况下执行面的逻辑
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(root._internalRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    unbatchedUpdates(() => {
      if (parentComponent != null) {
        root.legacy_renderSubtreeIntoContainer(
          parentComponent,
          children,
          callback,
        );
      } else {
        root.render(children, callback);
      }
    });
  } else {
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(root._internalRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    if (parentComponent != null) {
      root.legacy_renderSubtreeIntoContainer(
        parentComponent,
        children,
        callback,
      );
    } else {
      root.render(children, callback);
    }
  }
  return getPublicRootInstance(root._internalRoot);
}

先看看五个参数的传值情况,parentComponent为null、children及创建的React元素、container还是真实的DOM根节点、forceHydrate为false(这个参数表示是否为服务端渲染,可忽略)、然后就是回调函数了。

再看看内部的代码逻辑,root初始化的时候肯定为空,所以需要定义并赋值。定义的类型是Root,Root类型的内部属性结构如下所示:

type Root = {
  render(children: ReactNodeList, callback: ?() => mixed): Work,
  unmount(callback: ?() => mixed): Work,
  legacy_renderSubtreeIntoContainer(
    parentComponent: ?React$Component<any, any>,
    children: ReactNodeList,
    callback: ?() => mixed,
  ): Work,
  createBatch(): Batch,

  _internalRoot: FiberRoot,
};

root赋值的时候,调用了legacyCreateRootFromDOMContainer,并且将container的_reactRootContainer属性也进行了同样的赋值。这个方法从字面意思上理解是通过DOM Container创建root节点。先不考虑内部实现细节,只要知道会返回一个Root类型的对象即可,然后继续看下面的逻辑。

接下来的逻辑是判断callback是否为function类型,如果是的话就进行调用

然后执行了unbatchedUpdates方法,这个方法的源码如下所示,执行并返回了传进去的方法:

let isBatchingUpdates: boolean = false;
let isUnbatchingUpdates: boolean = false;

function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  if (isBatchingUpdates && !isUnbatchingUpdates) {
    isUnbatchingUpdates = true;
    try {
      return fn(a);
    } finally {
      isUnbatchingUpdates = false;
    }
  }
  return fn(a);
}

最终结果是执行了传入unbatchedUpdates的方法,又由于传入的parentComponent为null,所以,最终执行的是root.render(children, callback)。然后在legacyRenderSubtreeIntoContainer方法执行的最后,返回了getPublicRootInstance(root._internalRoot),最终会返回到ReactDOM.render方法中。

到这里,我们暂时忽略掉一些细节实现,大致知道了ReactDOM.render方法都执行了哪些内容。

React是如何创建Fiber树以及FiberRoot节点

虽然知道了大致的ReactDOM.render方法的执行流程,但是还有很多地方不太清除,所以我们要继续看一些重点步骤的实现细节逻辑。 首先是前面的legacyCreateRootFromDOMContainer方法到底做了些什么事情。然后是在unbatchedUpdates方法中执行的root.render(children, callback)又做了哪些处理,下面继续看源码的实现。

  • legacyCreateRootFromDOMContainer(container,forceHydrate,)

先看代码:

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): Root {
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {

      container.removeChild(rootSibling);
    }
  }
  // Legacy roots are not async by default.
  const isConcurrent = false;
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

传进去的是两个参数,只需要关心第一个参数,是真实的DOM节点,然后方法最后返回了一个ReactRoot实例。内部逻辑里面,shouldHydrate变量值为false,所以内部就是返回了一个ReactRoot实例。下面就看看ReactRoot是个什么类,它的render方法做了什么事情。

function ReactRoot(
  container: DOMContainer,
  isConcurrent: boolean,
  hydrate: boolean,
) {
  const root = createContainer(container, isConcurrent, hydrate);
  this._internalRoot = root;
}

export function createContainer(
  containerInfo: Container,
  isConcurrent: boolean,
  hydrate: boolean,
): OpaqueRoot {
  return createFiberRoot(containerInfo, isConcurrent, hydrate);
}
export function createFiberRoot(
  containerInfo: any,
  isConcurrent: boolean,
  hydrate: boolean,
): FiberRoot {
  const uninitializedFiber = createHostRootFiber(isConcurrent);

  let root;
  if (enableSchedulerTracing) {
    root = ({
      current: uninitializedFiber,
      containerInfo: containerInfo,
      pendingChildren: null,

      earliestPendingTime: NoWork,
      latestPendingTime: NoWork,
      earliestSuspendedTime: NoWork,
      latestSuspendedTime: NoWork,
      latestPingedTime: NoWork,

      pingCache: null,

      didError: false,

      pendingCommitExpirationTime: NoWork,
      finishedWork: null,
      timeoutHandle: noTimeout,
      context: null,
      pendingContext: null,
      hydrate,
      nextExpirationTimeToWorkOn: NoWork,
      expirationTime: NoWork,
      firstBatch: null,
      nextScheduledRoot: null,

      interactionThreadID: unstable_getThreadID(),
      memoizedInteractions: new Set(),
      pendingInteractionMap: new Map(),
    }: FiberRoot);
  } else {
    root = ({
      current: uninitializedFiber,
      containerInfo: containerInfo,
      pendingChildren: null,

      pingCache: null,

      earliestPendingTime: NoWork,
      latestPendingTime: NoWork,
      earliestSuspendedTime: NoWork,
      latestSuspendedTime: NoWork,
      latestPingedTime: NoWork,

      didError: false,

      pendingCommitExpirationTime: NoWork,
      finishedWork: null,
      timeoutHandle: noTimeout,
      context: null,
      pendingContext: null,
      hydrate,
      nextExpirationTimeToWorkOn: NoWork,
      expirationTime: NoWork,
      firstBatch: null,
      nextScheduledRoot: null,
    }: BaseFiberRootProperties);
  }

  uninitializedFiber.stateNode = root;
  return ((root: any): FiberRoot);
}

ReactRoot的构造函数,调用了createContainer方法,这个方法属于react-recociler部分的逻辑。这个方法最终又return了 createFiberRoot(containerInfo,isConcurrent,hydrate),createFiberRoot方法最终返回了一个FiberRoot类型的实例,并且其current属性的值是一个Fiber类型数据。而这个Fiber类型的数据是一个宿主根Fiber节点(HostRootFiber)。因为这里涉及到了fiber,暂时不深入这个部分(内容较多),这里可以知道最终在ReactRoot的构造函数里,root变量是一个FiberRoot类型的数据,而root.current是一个Fiber类型的节点,而且这个Fiber类型的节点是一个非常重要的东西,它是所有要渲染的内容的一个根节点,它通过childrensibling以及return属性构成了整个fiber树结构。大致如下图所示:

2020-03-08_215043

上图中地RootDOM就是真实的DOM根节点。

  • root.render(children, callback)的内部逻辑

接下来看看ReactRoot实例的render方法所执行的内容

ReactDOM.js

ReactRoot.prototype.render = function(
  children: ReactNodeList,
  callback: ?() => mixed,
): Work {
  const root = this._internalRoot;// fiber类型
  const work = new ReactWork();
  callback = callback === undefined ? null : callback;
  if (__DEV__) {
    warnOnInvalidCallback(callback, 'render');
  }
  if (callback !== null) {
    work.then(callback);
  }
  updateContainer(children, root, null, work._onCommit);
  return work;
};

ReactWork的实现

function ReactWork() {
  this._callbacks = null;
  this._didCommit = false;
  // TODO: Avoid need to bind by replacing callbacks in the update queue with
  // list of Work objects.
  this._onCommit = this._onCommit.bind(this);
}
ReactWork.prototype.then = function(onCommit: () => mixed): void {
  if (this._didCommit) {
    onCommit();
    return;
  }
  let callbacks = this._callbacks;
  if (callbacks === null) {
    callbacks = this._callbacks = [];
  }
  callbacks.push(onCommit);
};
ReactWork.prototype._onCommit = function(): void {
  if (this._didCommit) {
    return;
  }
  this._didCommit = true;
  const callbacks = this._callbacks;
  if (callbacks === null) {
    return;
  }
  // TODO: Error handling.
  for (let i = 0; i < callbacks.length; i++) {
    const callback = callbacks[i];
    callback();
  }
};

ReactFiberReconciler.js

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  const current = container.current;
  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

这部分开始稍微复杂一些,因为从这里开始,要涉及到很多fiber以及调度(schedule)的知识了。fiber可以理解为一个工作树,他是一个与react的元素结构对应的树结构,这个树拥有每一个react元素节点信息,同时还有所有side-effect效果的操作的信息在里面。这部分在后续的源码阅读里面再详细探讨,暂时先这么理解。

root.render()方法里面的执行逻辑,最重要的是最终调用了updateContainer方法,进而调用了updateContainerAtExpirationTime方法,这部分后续调用的链路还有很长(参考上面的render方法的流程图),会涉及到react的调度(schedule)部分的逻辑,涉及到fiber的工作队列等内容,最终会实现reactElement插入并挂在到DOM根节点上。这部分准备后续下一篇文章中进行详细研究和分享。

Next

本文主要梳理了调用ReactDOM.render方法的整个流程,大致经过了什么步骤,后续还有一部分关于最终渲染挂在到页面上的源码实现还没有进行详细分析,下一篇文章会接着updateContainer方法,梳理后续调用的所有方法的过程,以及最后React是如何实现将创建的react element挂载到DOM节点上的。

未完待续...

作者:kelvin 文章github地址:github.com/MEG-FEX/blo…