React渲染过程源码分析

什么是虚拟DOM(Virtual DOM)

在传统的开发模式中,每次须要进行页面更新的时候都须要咱们手动的更新DOM:html

在前端开发中,最应该避免的就是DOM的更新,由于DOM更新是极其耗费性能的,有过操做DOM经历的都应该知道,修改DOM的代码也很是冗长,也会致使项目代码阅读困难。在React中,把真是得DOM转换成JavaScript对象树,这就是咱们说的虚拟DOM,它并非真正的DOM,只是存有渲染真实DOM须要的属性的对象。前端

虚拟DOM的好处

虽然虚拟DOM会提高必定得性能可是并不明显,由于每次须要更新的时候Virtual DOM须要比较两次的DOM有什么不一样,而后批量更新,这也是须要资源的。node

Virtual真实的好处实际上是,他能够实现跨平台,咱们所熟知的react-native就是基于VirtualDOM来实现的。react

Virtual DOM实现

如今咱们根据源码来分析一下Virtual DOM的构建过程。json

JSX和React.createElementreact-native

在看源码以前,如今回顾一下React中建立组件的两种方式。缓存

1.JSX安全

function App() {
  return (
    <div>Hello React</div>
  );
}
复制代码

2.React.createElementbash

const App = React.createElement('div', null, 'Hello React');
复制代码

这里多说一句其实JSX只不过是React.createElement的语法糖,在编译的时候babel会将JSX转换成为使用React.createElement的形式,由于JSX语法更加符合咱们平常开发的习惯,因此咱们在写React的时候更多的是使用JSX语法进行编写。babel

React.createElement都作了什么

下面粘贴一段React.createElement的源码来分析:

ReactElement.createElement = function(type, config, children) {
  //初始化参数
  var propName;
  var props = {};
  var key = null;
  var ref = null;
  var self = null;
  var source = null;

  if (config != null) {
    // 若是存在config,则提取里面的内容
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 将新添加的元素更新到新的props中
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

    //若是只有一个children参数,那么指直接赋值给children
    //不然合并处理children
  var childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    var childArray = Array(childrenLength);
    for (var i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // 若是某个prop为空,且存在默认的prop,则将默认的prop赋值给props
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  
 //返回一个ReactElement实例对象,这个能够理解就是咱们说的虚拟DOM
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
};
复制代码

ReactElement与其中的安全机制

看到这里咱们不由好奇上述代码中返回的ReactElement究竟是个什么东西呢?其实ReactElement就只是咱们常说的虚拟DOM,ReactElement主要包含了这个DOM节点的类型(type)、属性(props)和子节点(children)。ReactElement只是包含了DOM节点的数据,尚未注入对应的一些方法来完成React框架的功能。

如今来看一下ReactElement的源码部分

var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    // react中防止XSS注入的变量,也是标志这个是react元素的变量,稍后会讲
    $$typeof: REACT_ELEMENT_TYPE,

    // 构建属于这个元素的属性值
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 记录一下建立这个元素的组件
    _owner: owner,
  };

  return element;
};
复制代码

上述代码能够看出来,ReactElement其实就是装有各类属性的一个大对象而已。

$$typeof

首先咱们如今控制台打印一下react.createElement的结果:

WHAT???这个变量是什么???

其实$$typeof是为了安全问题引入的变量,什么安全问题呢?那就是XSS

咱们都知道React.createElement方法的第三个参数是容许用户输入自定义组件的,那么设想一下,若是前端容许用户输入下面一段代码:

var input = "{"type": "div", "props": {"dangerouslySetInnerHTML": {"__html": "<script>alert('hey')</script>"}}}"" //而后咱们开始用输入的值建立ReactElement,就变成了下面这个样子 React.createElement('div', null, input); 复制代码

至此XSS注入就达成目的啦。

那么$$typeof这个变量是怎么作到安全认证的呢???

var REACT_ELEMENT_TYPE =
  (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
  0xeac7;
  
  ReactElement.isValidElement = function (object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
};
复制代码

首先$$typeof是Symbol类型的变量,是没法经过json对象转成字符串,因此就若是只是简单的json拷贝,是没有办法经过ReactElement.isValidElement的验证的,ReactElement.isValidElement会将不带有$$typeof变量的元素所有丢掉不用。

React的render过程

如今经过源码来看一下react中从定义完组件以后render到页面的过程。

1.ReactDOM.render

当咱们想要将一个组件渲染到页面上须要调用ReactDOM.render(element,container,[callback])方法,如今咱们就从这个方法入手一步一步来看源码:

var ReactDOM = {
  findDOMNode: findDOMNode,
  render: ReactMount.render,
  unmountComponentAtNode: ReactMount.unmountComponentAtNode,
  version: ReactVersion
};
复制代码

从上面代码咱们能够看到,咱们常常调用的ReactDOM.render,实际上是在调用ReactMount的render方法。因此咱们如今来看ReactMount中的render方法都作了些什么。

/src/renderers/dom/client/ReactMount.js

  render: function (nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(
      null,
      nextElement,
      container,
      callback,
    );
  }
复制代码

2._renderSubtreeIntoContainer

如今咱们终于找到了源头,那就是_renderSubtreeIntoContainer方法,咱们在来看一下它是怎么样定义的,能够根据下面代码中的注释一步一步的来看:

_renderSubtreeIntoContainer: function (
    parentComponent,
    nextElement,
    container,
    callback,
  ) {
    // 检验传入的callback是否符合标准,若是不符合,validateCallback会throw出
    //一个错误(内部调用了node_modules/fbjs/lib/invariant有invariant方法)
    ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');

    // 此处的TopLevelWrapper,只不过是将你传进来的type,进行一层包裹,并赋值ID,并会在TopLevelWrapper.render方法中返回你传入的值
    // 具体看源码,,因此个这东西只是一个包裹层
    var nextWrappedElement = React.createElement(TopLevelWrapper, {
      child: nextElement,
    });

    //判断以前是否渲染过此元素,若是有返回此元素,若是没有返回null
    var prevComponent = getTopLevelWrapperInContainer(container);

    if (prevComponent) {
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props.child;
      // 判断是否须要更新组件
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance();
        var updatedCallback =
          callback &&
          function () {
            callback.call(publicInst);
          };
        // 若是须要更新则调用组件更新方法,直接返回更新后的组件
        ReactMount._updateRootComponent(
          prevComponent,
          nextWrappedElement,
          nextContext,
          container,
          updatedCallback,
        );
        return publicInst;
      } else {
        // 不须要更新组件,那就把以前的组件卸载掉
        ReactMount.unmountComponentAtNode(container);
      }
    }

    // 返回当前容器的DOM节点,若是没有container返回null
    var reactRootElement = getReactRootElementInContainer(container);
    // 返回上面reactRootElement的data-reactid
    var containerHasReactMarkup =reactRootElement && !!internalGetID(reactRootElement);
    // 判断当前容器是否是有身为react元素的子元素
    var containerHasNonRootReactChild = hasNonRootReactChild(container);
    // 获得是否应该重复使用的标记变量
    var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;



    // 将一个新的组件渲染到真是得DOM上
    var component = ReactMount._renderNewRootComponent(
      nextWrappedElement,
      container,
      shouldReuseMarkup,
      nextContext,
    )._renderedComponent.getPublicInstance();

    // 若是有callback函数那就执行这个回调函数,而且将其this只想component
    if (callback) {
      callback.call(component);
    }

    // 返回组件
    return component;
  },
复制代码

根据上面的注释能够很容易理解上面的代码,如今咱们总结一下_renderSubtreeIntoContainer方法的执行过程:

1.校验传入callback的格式是否符合规范
2.用TopLevelWrapper包裹层(带有reactID)包裹传入的type,这里说明一下,react.createElement这个方法的type值能够有三种分别是,原生标签的标签名字符串('div'、'span')、react component 、react fragment
3.判断是否渲染过这次准备渲染的元素,若是渲染过,则判断是否须要更新。
    3.1 若是须要更新则调用更新方法,而且直接将更新后的组件返回
    3.2 若是不须要更新,则卸载老组件
4.若是没渲染过,则处理shouldReuseMarkup变量
5.调用ReactMount._renderNewRootComponent将组将更新到DOM(此函数后面会分析)
6.返回组件
复制代码

3.ReactMount._renderNewRootComponent(渲染组件,批次装载)

上面说到其实在_renderSubtreeIntoContainer方法中,最后使用了ReactMount._renderNewRootComponent进行进行组件的渲染,接下来咱们看一下该方法的源码:

_renderNewRootComponent: function (
    nextElement,
    container,
    shouldReuseMarkup,
    context,
  ) {
    // 监听window上面的滚动事件,缓存滚动变量,保证在滚动的时候页面不会触发重排
    ReactBrowserEventEmitter.ensureScrollValueMonitoring();
    //获取组件实例
    var componentInstance = instantiateReactComponent(nextElement, false);

    // 批处理,初始化render的过程是异步的,可是在render的时候componentWillMount或者componentDidMount生命中其中
    // 可能会执行更新变量的操做,这是react会将这些操做经过当前批次策略,统一处理。
    ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode, // *
      componentInstance,
      container,
      shouldReuseMarkup,
      context,
    );

    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;
    // 返回实例
    return componentInstance;
  }
复制代码

仍是先来总结一下上面代码的过程:

1.监听滚动事件,缓存变量,避免滚动带来的重排
2.初始化组件实例
3.批量执行更新操做
复制代码
react四大类组件

在上面代码执行过程的2中调用instantiateReactComponent建立了,组件的实例,其实组件类型有四种,具体看下图:

在这里咱们仍是看一下它的具体实现,而后分析一下过程:

function instantiateReactComponent(node, shouldHaveDebugID) {
  var instance;

  if (node === null || node === false) {
    // 空组件
    instance = ReactEmptyComponent.create(instantiateReactComponent);
  } else if (typeof node === 'object') {
    var element = node;
    if (typeof element.type === 'string') {
      // 原生DOM
      instance = ReactHostComponent.createInternalComponent(element);
    } else if (isInternalComponentType(element.type)) {
      instance = new element.type(element);
    } else {
      // react组件
      instance = new ReactCompositeComponentWrapper(element);
    }
  } else if (typeof node === 'string' || typeof node === 'number') {
    // 文本字符串
    instance = ReactHostComponent.createInstanceForText(node);
  } else {
    
  }
  return instance;
}

1.node为空时初始化空组件ReactEmptyComponent.create(instantiateReactComponent)
2.node类型是对象时,便是DOM标签或者自定义组件,那么若是element的类型是字符串,则初始化DOM标签组件ReactNativeComponent.createInternalComponent,不然初始化自定义组件ReactCompositeComponentWrapper
3.当node是字符串或者数字时,初始化文本组件ReactNativeComponent.createInstanceForText
4.其余状况不处理
复制代码
批次装载

在_renderNewRootComponent代码中有一个方法后面我是打了星号的,batchedUpdate方法的第一个参数实际上是个callback,这里也就是batchedMountComponentIntoNode,从方法名就能够很容易看出来他是一个批次装载组件的方法,他是定义在ReactMount上面的,来看一下他的具体实现吧。

function batchedMountComponentIntoNode(
  componentInstance,
  container,
  shouldReuseMarkup,
  context,
) {

  // 在batchedMountComponentIntoNode中,使用transaction.perform调用mountComponentIntoNode让其基于事务机制进行调用
  var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
    !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement,
  );
  transaction.perform(
    mountComponentIntoNode,
    null,
    componentInstance,
    container,
    transaction,
    shouldReuseMarkup,
    context,
  );
  ReactUpdates.ReactReconcileTransaction.release(transaction);
}
复制代码

事务机制之后再进行分析,这里就直接来看mountComponentIntoNode是如何将组件渲染成DOM节点的吧。

4.生成DOM(mountComponentIntoNode)

mountComponentIntoNode这个函数主要就是装载组件,而且将其插入到DOM中,话很少说,直接上源码,而后根据源码一步步的分析:

/**
 * Mounts this component and inserts it into the DOM.
 *
 * @param {ReactComponent} componentInstance The instance to mount.
 * @param {DOMElement} container DOM element to mount into.
 * @param {ReactReconcileTransaction} transaction
 * @param {boolean} shouldReuseMarkup If true, do not insert markup
 */

function mountComponentIntoNode(
  wrapperInstance,
  container,
  transaction,
  shouldReuseMarkup,
  context,
) {
  var markup = ReactReconciler.mountComponent(
    wrapperInstance,
    transaction,
    null,
    ReactDOMContainerInfo(wrapperInstance, container),
    context,
  );
  wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
  ReactMount._mountImageIntoNode(
    markup,
    container,
    wrapperInstance,
    shouldReuseMarkup,
    transaction,
  );
}
复制代码

能够看到mountComponentIntoNode方法首先调用了ReactReconciler.mountComponent方法,而在ReactReconciler.mountComponent方法中实际上是调用了上面四种react组件的mountComponent方法,前面的就不说了,咱们直接来看一下四种组件中的mountComponent方法都干了什么吧。

/src/renderers/dom/shared/ReactDOMComponent.js
  mountComponent: function (
    transaction,
    hostParent,
    hostContainerInfo,
    context,
  ) {
    var props = this._currentElement.props;
    switch (this._tag) {
      case 'audio':
      case 'form':
      case 'iframe':
      case 'img':
      case 'link':
      case 'object':
      case 'source':
      case 'video':

      ....

    // 建立容器
    var mountImage;
      var ownerDocument = hostContainerInfo._ownerDocument;
      var el;
      if (this._tag === 'script') {
          var div = ownerDocument.createElement('div');
          var type = this._currentElement.type;
          div.innerHTML = `<${type}></${type}>`;
          el = div.removeChild(div.firstChild);
        } else if (props.is) {
          el = ownerDocument.createElement(this._currentElement.type, props.is);
        } else {
          el = ownerDocument.createElement(this._currentElement.type);
        }
      }

      // 更新props,第一个参数是上次的props,第二个参数是最新的props,若是上一次的props为空那么就是新建状态
      this._updateDOMProperties(null, props, transaction);
      // 生成DOMLazyTree对象
      var lazyTree = DOMLazyTree(el);
      // 处理孩子节点
      this._createInitialChildren(transaction, props, context, lazyTree);
      mountImage = lazyTree;
    
    // 返回容器
    return mountImage;
  }
复制代码

总结一下上述代码的执行过程,在这里我只截取了初次渲染时候执行的代码: 1.对特殊的标签进行处理,而且调用方法给出相应警告 2.建立DOM节点 3.调用_updateDOMProperties方法来处理props 4.生成DOMLazyTree 5.经过DOMLazyTree调用_createInitialChildren处理孩子节点。而后返回DOM节点

下面咱们来看一下这个DOMLazyTree方法都干了些什么,仍是上源码:

function queueChild(parentTree, childTree) {
  if (enableLazy) {
    parentTree.children.push(childTree);
  } else {
    parentTree.node.appendChild(childTree.node);
  }
}

function queueHTML(tree, html) {
  if (enableLazy) {
    tree.html = html;
  } else {
    setInnerHTML(tree.node, html);
  }
}

function queueText(tree, text) {
  if (enableLazy) {
    tree.text = text;
  } else {
    setTextContent(tree.node, text);
  }
}

function toString() {
  return this.node.nodeName;
}

function DOMLazyTree(node) {
  return {
    node: node,
    children: [],
    html: null,
    text: null,
    toString,
  };
}

DOMLazyTree.queueChild = queueChild;
DOMLazyTree.queueHTML = queueHTML;
DOMLazyTree.queueText = queueText;
复制代码

从上述代码能够看到DOMLazyTree其实就是一个用来包裹节点信息的对象,里面有孩子节点,html节点,文本节点,而且提供了将这些节点插入到真是DOM中的方法,如今咱们来看一下在_createInitialChildren方法中它是如何来使用这个lazyTree对象的:

_createInitialChildren: function (transaction, props, context, lazyTree) {
    var innerHTML = props.dangerouslySetInnerHTML;
    if (innerHTML != null) {
      if (innerHTML.__html != null) {
        DOMLazyTree.queueHTML(lazyTree, innerHTML.__html);
      }
    } else {
      var contentToUse = CONTENT_TYPES[typeof props.children]
        ? props.children
        : null;
      var childrenToUse = contentToUse != null ? null : props.children;
      if (contentToUse != null) {
        if (contentToUse !== '') {
          DOMLazyTree.queueText(lazyTree, contentToUse);
        }
      } else if (childrenToUse != null) {
        var mountImages = this.mountChildren(
          childrenToUse,
          transaction,
          context,
        );
        for (var i = 0; i < mountImages.length; i++) {
          DOMLazyTree.queueChild(lazyTree, mountImages[i]);
        }
      }
    }
  }
复制代码

判断当前节点的dangerouslySetInnerHTML属性、孩子节点是否为文本和其余节点分别调用DOMLazyTree的queueHTML、queueText、queueChild.

ReactCompositeComponent

在实例调用mountComponent时,在这里额外的说一下这个函数的执行过程,ReactCompositeComponent也就是咱们说的react自定义组件,起主要的执行过程以下:

1.处理props、contex等变量,调用构造函数建立组件实例
2.判断是否为无状态组件,处理state
3.调用performInitialMount生命周期,处理子节点,获取markup。
4.调用componentDidMount生命周期
复制代码

在performInitialMount函数中,首先调用了componentWillMount生命周期,因为自定义的React组件并非一个真实的DOM,因此在函数中又调用了孩子节点的mountComponent。这也是一个递归的过程,当全部孩子节点渲染完成后,返回markup并调用componentDidMount.

5.渲染DOM(_mountImageIntoNode)

在上述mountComponentIntoNode中最后一步是执行_mountImageIntoNode方法,在该方法中核心的渲染方法就是insertTreeBefore,咱们直接来看这个方法的源码,而后进行分析:

var insertTreeBefore = function(
  parentNode,
  tree,
  referenceNode,
) {

  if (
    tree.node.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE ||
    (tree.node.nodeType === ELEMENT_NODE_TYPE &&
      tree.node.nodeName.toLowerCase() === 'object' &&
      (tree.node.namespaceURI == null ||
        tree.node.namespaceURI === DOMNamespaces.html))
  ) {
    insertTreeChildren(tree);
    parentNode.insertBefore(tree.node, referenceNode);
  } else {
    parentNode.insertBefore(tree.node, referenceNode);
    insertTreeChildren(tree);
  }
}

function insertTreeChildren(tree) {
  if (!enableLazy) {
    return;
  }
  var node = tree.node;
  var children = tree.children;
  if (children.length) {
    for (var i = 0; i < children.length; i++) {
      insertTreeBefore(node, children[i], null);
    }
  } else if (tree.html != null) {
    setInnerHTML(node, tree.html);
  } else if (tree.text != null) {
    setTextContent(node, tree.text);
  }
}
复制代码

1.该方法首先就是判断当前节点是否是fragment节点或者Object插件 2.若是知足条件1,首先调用insertTreeChildren将此节点的孩子节点渲染到当前节点上,再将渲染完的节点插入到html 3.若是不知足1,是其余节点,先将节点插入到插入到html,再调用insertTreeChildren将孩子节点插入到html

在此过程当中已经一次调用了setInnerHTML或setTextContent来分别渲染html节点和文本节点。

结尾

上述文章就是react的初次渲染过程分析,若是有哪些地方写的不对,欢迎在评论中讨论。本文代码采用的react15中的代码,和react最新版本代码会有一些的出入。

相关文章
相关标签/搜索