React渲染过程源码分析

栏目: 服务器 · 发布时间: 6年前

内容简介:在传统的开发模式中,每次需要进行页面更新的时候都需要我们手动的更新DOM:在前端开发中,最应该避免的就是DOM的更新,因为DOM更新是极其耗费性能的,有过操作DOM经历的都应该知道,修改DOM的代码也非常冗长,也会导致项目代码阅读困难。在React中,把真是得DOM转换成JavaScript对象树,这就是我们说的虽然虚拟DOM会提升一定得性能但是并不明显,因为每次需要更新的时候Virtual DOM需要比较两次的DOM有什么不同,然后批量更新,这也是需要资源的。

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

React渲染过程源码分析

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

React渲染过程源码分析

虚拟DOM的好处

虽然虚拟DOM会提升一定得性能但是并不明显,因为每次需要更新的时候Virtual DOM需要比较两次的DOM有什么不同,然后批量更新,这也是需要资源的。

Virtual真实的好处其实是,他可以实现跨平台,我们所熟知的react-native就是基于VirtualDOM来实现的。

Virtual DOM实现

现在我们根据源码来分析一下Virtual DOM的构建过程。

JSX和React.createElement

在看源码之前,现在回顾一下React中创建组件的两种方式。

1.JSX

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

2.React.createElement

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

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

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的结果:

React渲染过程源码分析

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创建了,组件的实例,其实组件类型有四种,具体看下图:

React渲染过程源码分析

在这里我们还是看一下它的具体实现,然后分析一下过程:

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.处理propscontex等变量,调用构造函数创建组件实例
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最新版本代码会有一些的出入。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Computation and Programming Using Python

Introduction to Computation and Programming Using Python

John V. Guttag / The MIT Press / 2013-7 / USD 25.00

This book introduces students with little or no prior programming experience to the art of computational problem solving using Python and various Python libraries, including PyLab. It provides student......一起来看看 《Introduction to Computation and Programming Using Python》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具