React组件插入DOM流程

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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,273评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,905评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,281评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,817评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,690评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,491评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,886评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,513评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,810评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,839评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,642评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,455评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,901评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,091评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,381评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,895评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,104评论 2 338