美文网首页
React组件插入DOM流程

React组件插入DOM流程

作者: Dabao123 | 来源:发表于2017-11-14 20:46 被阅读481次

1 简介

React广受好评的一个重要原因就是组件化开发,一方面分模块的方式便于协同开发,降低耦合,后期维护也轻松;另一方面使得一次开发,多处复用成为现实,甚至可以直接复用开源React组件。开发完一个组件后,我们需要插入DOM中,一般使用如下代码

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('example')
);

经过babel转码后为

ReactDOM.render(
        React.createElement(
          'h1',   // type, DOM原生组件的type为string,React自定义组件type为Object
          null,   // config,会设置到ref,key,props中
          'Hello, world!'   // children,子组件.这儿为文本组件
        ),
        document.getElementById('example')
)

那么React底层是怎么将组件插入DOM中的呢。本文来详细分析它的前因后果。

2 ReactMount._renderSubtreeIntoContainer()

ReactDOM.render()实际调用ReactMount.render(),接着调用到ReactMount._renderSubtreeIntoContainer().
这个调用链比较简单,不分析了。下面重点分析_renderSubtreeIntoContainer(). 我们去除掉开发调试阶段的报错代码(比如 “development” !== ‘production’)。

/**
   * 将ReactElement插入DOM中,并返回ReactElement对应的ReactComponent。
   * ReactElement是React元素在内存中的表示形式,可以理解为一个数据类,包含type,key,refs,props等成员变量
   * ReactComponent是React元素的操作类,包含mountComponent(), updateComponent()等很多操作组件的方法
   *
   * @param {parentComponent} 父组件,对于第一次渲染,为null
   * @param {nextElement} 要插入到DOM中的组件,对应上面例子中的<h1>Hello, world!</h1>经过babel转译后的元素
   * @param {container} 要插入到的容器,对应上面例子中的document.getElementById('example')获取的DOM对象
   * @param {callback} 第一次渲染为null
   *
   * @return {component}  返回ReactComponent,对于ReactDOM.render()调用,不用管返回值。
   */ 
_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
    // 刚开始一段开发阶段的报错代码,省去
    ...

    // 包装ReactElement,将nextElement挂载到wrapper的props属性下,这段代码不是很关键
    var nextWrappedElement = ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
    // 获取要插入到的容器的前一次的ReactComponent,这是为了做DOM diff
    // 对于ReactDOM.render()调用,prevComponent为null
    var prevComponent = getTopLevelWrapperInContainer(container);

    if (prevComponent) {
      // 从prevComponent中获取到prevElement这个数据对象。一定要搞清楚ReactElement和ReactComponent的作用,他们很关键
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props;
      // DOM diff精髓,同一层级内,type和key不变时,只用update就行。否则先unmount组件再mount组件
      // 这是React为了避免递归太深,而做的DOM diff前提假设。它只对同一DOM层级,type相同,key(如果有)相同的组件做DOM diff,否则不用比较,直接先unmount再mount。这个假设使得diff算法复杂度从O(n^3)降低为O(n).
      // shouldUpdateReactComponent源码请看后面的分析
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance();
        var updatedCallback = callback && function () {
          callback.call(publicInst);
        };
        // 只需要update,调用_updateRootComponent,然后直接return了
        ReactMount._updateRootComponent(prevComponent, nextWrappedElement, container, updatedCallback);
        return publicInst;
      } else {
        // 不做update,直接先卸载再挂载。即unmountComponent,再mountComponent。mountComponent在后面代码中进行
        ReactMount.unmountComponentAtNode(container);
      }
    }

    var reactRootElement = getReactRootElementInContainer(container);
    var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
    var containerHasNonRootReactChild = hasNonRootReactChild(container);

    var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
    // 初始化,渲染组件,然后插入到DOM中。_renderNewRootComponent很关键,后面详细分析
    var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, parentComponent != null ? parentComponent._reactInternalInstance._processChildContext(parentComponent._reactInternalInstance._context) : emptyObject)._renderedComponent.getPublicInstance();
    // render方法中带入的回调,ReactDOM.render()调用时一般不传入
    if (callback) {
      callback.call(component);
    }
    return component;
  },

shouldUpdateReactComponent()源码如下:

function shouldUpdateReactComponent(prevElement, nextElement) {
  // 前后两次ReactElement中任何一个为null,则必须另一个为null才返回true。这种情况一般不会碰到
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }

  var prevType = typeof prevElement;
  var nextType = typeof nextElement;

  // React DOM diff算法
  if (prevType === 'string' || prevType === 'number') {
    // 如果前后两次为数字或者字符,则认为只需要update(处理文本元素),返回true
    return (nextType === 'string' || nextType === 'number');
  } else {
      // 如果前后两次为DOM元素或React元素,则必须type和key不变(key用于listView等组件,很多时候我们没有设置key,故只需type相同)才update,否则先unmount再重新mount。返回false
    return (
      nextType === 'object' &&
      prevElement.type === nextElement.type &&
      prevElement.key === nextElement.key
    );
  }
}

3.ReactMount._renderNewRootComponent

_renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
    ReactBrowserEventEmitter.ensureScrollValueMonitoring();
    // 初始化ReactComponent,根据ReactElement中不同的type字段,创建不同类型的组件对象,即ReactComponent
    // 前一篇文章中已经分析了。http://blog.csdn.net/u013510838/article/details/55669769
    var componentInstance = instantiateReactComponent(nextElement);

    // 处理batchedMountComponentIntoNode方法调用,将ReactComponent插入DOM中,后面详细分析
    ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);

    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;

    return componentInstance;
  },

batchedMountComponentIntoNode以transaction事务的形式调用mountComponentIntoNode,源码分析如下。

function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReuseMarkup, context) {
  var markerName;
  // 一段log,可以不管
  if (ReactFeatureFlags.logTopLevelRenders) {
    var wrappedElement = wrapperInstance._currentElement.props;
    var type = wrappedElement.type;
    markerName = 'React mount: ' + (typeof type === 'string' ? type : type.displayName || type.name);
    console.time(markerName);
  }

  // 调用对应ReactComponent中的mountComponent方法来渲染组件,这个是React生命周期的重要方法。后面详细分析。
  // mountComponent返回React组件解析的HTML。不同的ReactComponent的mountComponent策略不同,可以看做多态
  // 上面的<h1>Hello, world!</h1>, 对应的是ReactDOMTextComponent,最终解析成的HTML为
  // <h1 data-reactroot="">Hello, world!</h1>
  var markup = ReactReconciler.mountComponent(wrapperInstance, transaction, null, ReactDOMContainerInfo(wrapperInstance, container), context);

  if (markerName) {
    console.timeEnd(markerName);
  }

  wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
  // 将解析出来的HTML插入DOM中
  ReactMount._mountImageIntoNode(markup, container, wrapperInstance, shouldReuseMarkup, transaction);
}

_mountImageIntoNode源码如下

 _mountImageIntoNode: function (markup, container, instance, shouldReuseMarkup, transaction) {
    // 对于ReactDOM.render()调用,shouldReuseMarkup为false
    if (shouldReuseMarkup) {
      ...
    }

    if (transaction.useCreateElement) {
      // 清空container的子节点,这个地方不明白为什么这么做
      while (container.lastChild) {
        container.removeChild(container.lastChild);
      }
      DOMLazyTree.insertTreeBefore(container, markup, null);
    } else {
      // 将markup这个HTML设置到container这个DOM元素的innerHTML属性上,这样就插入到了DOM中了
      setInnerHTML(container, markup);
      // 将instance这个ReactComponent渲染后的对象,即Virtual DOM,保存到container这个DOM元素的firstChild这个原生节点上。简单理解就是将Virtual DOM保存到内存中,这样可以大大提高交互效率
      ReactDOMComponentTree.precacheNode(instance, container.firstChild);
    }
  }

4 总结

ReactDOM.render()是渲染React组件并插入到DOM中的入口方法,它的执行流程大概为

1.React.createElement(),创建ReactElement对象。�他的重要的成员变量有type,key,ref,props。这个过程中会调用getInitialState(), 初始化state,只在挂载的时候才调用。后面update时不再调用了。

2.instantiateReactComponent(),根据ReactElement的type分别创建ReactDOMComponent, ReactCompositeComponent,ReactDOMTextComponent等对象

3.mountComponent(), 调用React生命周期方法解析组件,得到它的HTML。

4._mountImageIntoNode(), 将HTML插入到DOM父节点中,通过设置DOM父节点的innerHTML属性。

5.缓存节点在React中的对应对象,即Virtual DOM。

相关文章

  • React组件插入DOM流程

    1 简介 React广受好评的一个重要原因就是组件化开发,一方面分模块的方式便于协同开发,降低耦合,后期维护也轻松...

  • react生命周期

    react的生命周期可以分为三个状态 Mounting:组件挂载,已插入真实DOM Updating:组件更新,正...

  • React 更新机制

    React 的更新流程 React 的渲染流程是: JSX → 虚拟 DOM → 真实 DOM React 的更新...

  • React生命周期

    React生命周期分为装载、更新、卸载、异常捕获 装载(Mounting) 组件被插入到DOM中 getDefau...

  • React Native —— Component(组件)

    React利用JSX语法将html标签封装成组件的形式,来插入到DOM中,可以很方便的构建出网页UI。在React...

  • 02 react 安装

    1.运行 cnpm i react react-dom -S 安装包react : 专门英语创建组件和虚拟DOM ...

  • 在项目中使用react

    1、运行cnpm i react react-dom -S 安装包 react:专门用于创建组件和虚拟DOM的,同...

  • React学习知识小总结

    react依赖: react react-dom babel-preset-react 组件自身状态设置初始状态:...

  • React学习笔记_01

    React 基础组件 react概述 npm i react react-dom react包 是核心,提供创建元...

  • React - 渲染最基本的虚拟DOM到页面上

    安装React相关包 npm install react react-dom react: 专门用于创建组件和虚拟...

网友评论

      本文标题:React组件插入DOM流程

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