美文网首页
React源码学习系列(二)—— ReactDOM.render

React源码学习系列(二)—— ReactDOM.render

作者: 邱鹏城 | 来源:发表于2018-02-23 13:59 被阅读0次

    概述

    上一篇讲到React中的元素(ReactElement的“实例”)会有一个type属性,而该值将决定其被渲染时的处理结果。
    ReactDOM.render实际即为React初次将vdom渲染至真实dom树的过程,其中包括了创建元素、添加属性、绑定事件等等操作。
    本篇,我们就通过ReactDOM.render的源码来了解一下其处理过程。

    ReactDOM.render方法使用

    首先看ReactDOM.render的使用方式:

    const App = (<h2>Hello World!</h2>)
    ReactDOM.render(App, document.querySelector('#app'))
    

    或者

    class App extends React.Component {
      render(){
        return (
          <div>
            <h2>Hello World!</h2>
          </div>
        )
      }
    }
    ReactDOM.render(<App />, document.querySelector('#app'))
    

    根据我们上一篇的讨论,我们知道上面两个例子中ReactDOM.render第一个参数传入的都是ReactElement的“实例”。

    而当第一个参数传入一个字符串类型,如下:

    ReactDOM.render('This is String', document.querySelector('#app'))
    
    // Uncaught Error: ReactDOM.render(): Invalid component element. Instead of passing a string like 'div', pass React.createElement('div') or <div />.
    

    可见,ReactDOM.render第一个参数不支持字符串类型,即不会直接创建 TextNode 插入到第二个参数指定的容器中。

    接下来,我们一起进入到源码中查看该方法。

    源码结构

    查看ReactDOM.js文件,可以看到ReactDOM.render引用ReactMount.jsrender方法,如下:

    ReactMount = {
      // ReactDOM.render直接引用此方法
      render: function (nextElement, container, callback) {
        return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
      },
      // 实际执行render的方法
      _renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
        ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');
    
        // 将传入的element用TopLevelWrapper包装,
        // 包装后的元素,标记有rootID,并且拥有render方法,
        // 具体可看TopLevelWrapper的源码
        var nextWrappedElement = React.createElement(TopLevelWrapper, {
          child: nextElement
        });
    
        // ReactDOM.render方法调用时,parentComponent为null
        var nextContext;
        if (parentComponent) {
          var parentInst = ReactInstanceMap.get(parentComponent);
          nextContext = parentInst._processChildContext(parentInst._context);
        } else {
          nextContext = emptyObject;
        }
    
        // 第一次执行时,prevComponent为null,具体可看此方法源码
        var prevComponent = getTopLevelWrapperInContainer(container);
    
        if (prevComponent) {
          var prevWrappedElement = prevComponent._currentElement;
          var prevElement = prevWrappedElement.props.child;
    
          // 判断上一次的prevElement和nextElement是否是同一个组件,或者仅仅是数字、字符串,如果是,则直接update,
          // 否则,重新渲染整个Element
          if (shouldUpdateReactComponent(prevElement, nextElement)) {
            var publicInst = prevComponent._renderedComponent.getPublicInstance();
            var updatedCallback = callback && function () {
              callback.call(publicInst);
            };
            // 更新vdom
            ReactMount._updateRootComponent(prevComponent, nextWrappedElement, nextContext, container, updatedCallback);
            return publicInst;
          } else {
            ReactMount.unmountComponentAtNode(container);
          }
        }
    
        var reactRootElement = getReactRootElementInContainer(container);
        var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
        var containerHasNonRootReactChild = hasNonRootReactChild(container);
    
    
        var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
        // 本次为首次渲染,因此调用ReactMount._renderNewRootComponent
        var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
        if (callback) {
          callback.call(component);
        }
        return component;
      },
      /**
       * Render a new component into the DOM. Hooked by hooks!
       *
       * @param {ReactElement} nextElement element to render
       * @param {DOMElement} container container to render into
       * @param {boolean} shouldReuseMarkup if we should skip the markup insertion
       * @return {ReactComponent} nextComponent
       */
      _renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
    
        ReactBrowserEventEmitter.ensureScrollValueMonitoring();
        // 初始化组件实例,并增加组件挂载(mount)、更新(update)、卸载(unmount)等方法
        var componentInstance = instantiateReactComponent(nextElement, false);
    
        // The initial render is synchronous but any updates that happen during
        // rendering, in componentWillMount or componentDidMount, will be batched
        // according to the current batching strategy.
    
        ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
    
        var wrapperID = componentInstance._instance.rootID;
        instancesByReactRootID[wrapperID] = componentInstance;
    
        return componentInstance;
      },
    }
    

    从以上代码可以看出,当调用ReactDOM.render时,使用TopLevelWrapper对element进行包装,随后将其传入ReactMount._renderNewRootComponent中,在此方法内,调用instantiateReactComponent组件的实例,该实例拥有mountComponent等挂载、更新的方法。

    接下来学习instantiateReactComponent的源码,源码位置位于instantiateReactComponent.js文件。

    /**
     * Given a ReactNode, create an instance that will actually be mounted.
     *
     * @param {ReactNode} node
     * @param {boolean} shouldHaveDebugID
     * @return {object} A new instance of the element's constructor.
     * @protected
     */
    function instantiateReactComponent(node, shouldHaveDebugID) {
      var instance;
    
      if (node === null || node === false) {
        instance = ReactEmptyComponent.create(instantiateReactComponent);
      } else if (typeof node === 'object') {
        var element = node;
        var type = element.type;
    
        // 代码块(1)
        // Special case string values
        if (typeof element.type === 'string') {
          // type为string的,调用createInternalComponent方法,
          // 对节点进行处理,包含属性、默认事件等等
          instance = ReactHostComponent.createInternalComponent(element); // (2)
        } else if (isInternalComponentType(element.type)) {
          // 内置type?
          // This is temporarily available for custom components that are not string
          // representations. I.e. ART. Once those are updated to use the string
          // representation, we can drop this code path.
          instance = new element.type(element);
    
          // We renamed this. Allow the old name for compat. :(
          if (!instance.getHostNode) {
            instance.getHostNode = instance.getNativeNode;
          }
        } else {
          // 其余的均为自定义组件, 通过此方法,创建组件实例
          // 此方法比较复杂
          instance = new ReactCompositeComponentWrapper(element);
        }
      } else if (typeof node === 'string' || typeof node === 'number') {
        // 字符串或数字,直接调用 createInstanceForText,生成实例
        instance = ReactHostComponent.createInstanceForText(node);
      } else {
        !false ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Encountered invalid React node of type %s', typeof node) : _prodInvariant('131', typeof node) : void 0;
      }
    
      // These two fields are used by the DOM and ART diffing algorithms
      // respectively. Instead of using expandos on components, we should be
      // storing the state needed by the diffing algorithms elsewhere.
      // 与diff算法相关,TOREAD...
      instance._mountIndex = 0;
      instance._mountImage = null;
    
      return instance;
    }
    

    结合注释细读以上代码,如代码块(1)中,根据nodetype类型来渲染节点,也即本文一开始所提到的type。为更好理解,我们使用以下代码渲染一个input元素:

    /**
     * 以下JSX相当于:
     * const inputEle = React.createElement('input', {
     *  defaultValue: '10',
     *  onClick: () => console.log('clicked')
     * })
    */
    const inputEle = (
      <input
        defaultValue="10"
        onClick={() => console.log('clicked')}
      />
    )
    
    ReactDOM.render(inputEle, document.getElementById('app'))
    

    根据我们上一篇所讲,inputEleReactElement的一个实例,其type属性为input
    因此,在instantiateReactComponent方法中,应该执行(2)处的分支,即:ReactHostComponent.createInternalComponent(element)
    我们查看ReactHostComponent.js文件,可看到createInternalComponent方法,代码如下:

    /**
     * Get a host internal component class for a specific tag.
     *
     * @param {ReactElement} element The element to create.
     * @return {function} The internal class constructor function.
     */
    function createInternalComponent(element) {
      !genericComponentClass ? process.env.NODE_ENV !== 'production' ? invariant(false, 'There is no registered component for the tag %s', element.type) : _prodInvariant('111', element.type) : void 0;
      return new genericComponentClass(element);
    }
    

    即返回genericComponentClass的一个实例,而genericComponentClass的来源,追寻源码,可以找到在ReactDefaultInjection中找到,实际上将ReactDOMComponent注入进来。

    ReactDOM源码中,作者将各种类型(如ReactEventListener、ReactDOMComponent等)抽象后通过Injection机制注入,我的理解是这样方便未来将类型整体升级替换,并且能一定程度上解耦(只需要保证类型对外提供的接口一致)。不知道是否理解有误... ...还望指教。

    因此instantiateReactComponent的代码(2)处实际返回:new ReactDOMComponent(node)
    接下来阅读ReactDOMComponent.js文件:
    先看ReactDOMComponent这个方法:

    /**
     * Creates a new React class that is idempotent and capable of containing other
     * React components. It accepts event listeners and DOM properties that are
     * valid according to `DOMProperty`.
     *
     *  - Event listeners: `onClick`, `onMouseDown`, etc.
     *  - DOM properties: `className`, `name`, `title`, etc.
     *
     * The `style` property functions differently from the DOM API. It accepts an
     * object mapping of style properties to values.
     *
     * @constructor ReactDOMComponent
     * @extends ReactMultiChild
     */
    function ReactDOMComponent(element) {
      var tag = element.type;
      validateDangerousTag(tag);
      this._currentElement = element;
      this._tag = tag.toLowerCase();
      this._namespaceURI = null;
      this._renderedChildren = null;
      this._previousStyle = null;
      this._previousStyleCopy = null;
      this._hostNode = null;
      this._hostParent = null;
      this._rootNodeID = 0;
      this._domID = 0;
      this._hostContainerInfo = null;
      this._wrapperState = null;
      this._topLevelWrapper = null;
      this._flags = 0;
      if (process.env.NODE_ENV !== 'production') {
        this._ancestorInfo = null;
        setAndValidateContentChildDev.call(this, null);
      }
    }
    
    _assign(ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mixin)
    

    以上代码可以看到,ReactDOMComponent这个类继承了ReactMultiChildMixin
    元素挂载时,实际调用:ReactDOMComponent.Mixin中的mountComponent方法,整体源码如下:

     /**
       * Generates root tag markup then recurses. This method has side effects and
       * is not idempotent.
       *
       * @internal
       * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
       * @param {?ReactDOMComponent} the parent component instance
       * @param {?object} info about the host container
       * @param {object} context
       * @return {string} The computed markup.
       */
      mountComponent: function (transaction, hostParent, hostContainerInfo, context) {
        this._rootNodeID = globalIdCounter++;
        this._domID = hostContainerInfo._idCounter++;
        this._hostParent = hostParent;
        this._hostContainerInfo = hostContainerInfo;
    
        var props = this._currentElement.props;
        // 调整props至DOM的合法属性,并且处理事件
        switch (this._tag) {
          case 'audio':
          case 'form':
          case 'iframe':
          case 'img':
          case 'link':
          case 'object':
          case 'source':
          case 'video':
            this._wrapperState = {
              listeners: null
            };
            transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
            break;
          case 'input':
            ReactDOMInput.mountWrapper(this, props, hostParent);
            props = ReactDOMInput.getHostProps(this, props);
            transaction.getReactMountReady().enqueue(trackInputValue, this);
            transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
            break;
          case 'option':
            ReactDOMOption.mountWrapper(this, props, hostParent);
            props = ReactDOMOption.getHostProps(this, props);
            break;
          case 'select':
            ReactDOMSelect.mountWrapper(this, props, hostParent);
            props = ReactDOMSelect.getHostProps(this, props);
            transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
            break;
          case 'textarea':
            ReactDOMTextarea.mountWrapper(this, props, hostParent);
            props = ReactDOMTextarea.getHostProps(this, props);
            transaction.getReactMountReady().enqueue(trackInputValue, this);
            transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
            break;
        }
    
        assertValidProps(this, props);
    
        // We create tags in the namespace of their parent container, except HTML
        // tags get no namespace.
        var namespaceURI;
        var parentTag;
        if (hostParent != null) {
          namespaceURI = hostParent._namespaceURI;
          parentTag = hostParent._tag;
        } else if (hostContainerInfo._tag) {
          namespaceURI = hostContainerInfo._namespaceURI;
          parentTag = hostContainerInfo._tag;
        }
        if (namespaceURI == null || namespaceURI === DOMNamespaces.svg && parentTag === 'foreignobject') {
          namespaceURI = DOMNamespaces.html;
        }
        if (namespaceURI === DOMNamespaces.html) {
          if (this._tag === 'svg') {
            namespaceURI = DOMNamespaces.svg;
          } else if (this._tag === 'math') {
            namespaceURI = DOMNamespaces.mathml;
          }
        }
        this._namespaceURI = namespaceURI;
    
        var mountImage;
        if (transaction.useCreateElement) {
          var ownerDocument = hostContainerInfo._ownerDocument;
          var el;
          if (namespaceURI === DOMNamespaces.html) {
            if (this._tag === 'script') {
              // Create the script via .innerHTML so its "parser-inserted" flag is
              // set to true and it does not execute
              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 {
              // Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
              // See discussion in https://github.com/facebook/react/pull/6896
              // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
              el = ownerDocument.createElement(this._currentElement.type);
            }
          } else {
            el = ownerDocument.createElementNS(namespaceURI, this._currentElement.type);
          }
          ReactDOMComponentTree.precacheNode(this, el);
          this._flags |= Flags.hasCachedChildNodes;
          if (!this._hostParent) {
            DOMPropertyOperations.setAttributeForRoot(el);
          }
          this._updateDOMProperties(null, props, transaction);
          var lazyTree = DOMLazyTree(el);
          this._createInitialChildren(transaction, props, context, lazyTree);
          mountImage = lazyTree;
        } else {
          var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
          var tagContent = this._createContentMarkup(transaction, props, context);
          if (!tagContent && omittedCloseTags[this._tag]) {
            mountImage = tagOpen + '/>';
          } else {
            mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
          }
        }
    
        switch (this._tag) {
          case 'input':
            transaction.getReactMountReady().enqueue(inputPostMount, this);
            if (props.autoFocus) {
              transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
            }
            break;
          case 'textarea':
            transaction.getReactMountReady().enqueue(textareaPostMount, this);
            if (props.autoFocus) {
              transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
            }
            break;
          case 'select':
            if (props.autoFocus) {
              transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
            }
            break;
          case 'button':
            if (props.autoFocus) {
              transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
            }
            break;
          case 'option':
            transaction.getReactMountReady().enqueue(optionPostMount, this);
            break;
        }
    
        return mountImage;
      }
    

    阅读上述代码,可以知道React是如何将一个ReactElement与DOM进行映射的(本例子只展示了DOMComponent这种类型,自定义组件、textNode这两种可自行找到源码阅读)。
    上述方法返回的值将会被传入ReactUpdates.batchedUpdates中进行挂载,这部分内容较为复杂,在未来将进一步解读。

    相关文章

      网友评论

          本文标题:React源码学习系列(二)—— ReactDOM.render

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