美文网首页
React渲染过程源码分析

React渲染过程源码分析

作者: A长安 | 来源:发表于2019-06-04 01:08 被阅读0次

    什么是虚拟DOM(Virtual DOM)

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

    image

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

    image

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

    image

    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.返回组件
    

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

    image

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

    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节点的吧。

    mountComponentIntoNode(生成DOM)

    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.

    渲染DOM

    在上述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最新版本代码会有一些的出入

    相关文章

      网友评论

          本文标题:React渲染过程源码分析

          本文链接:https://www.haomeiwen.com/subject/zgskxctx.html