React組件插入DOM流程

1 簡(jiǎn)介

React廣受好評(píng)的一個(gè)重要原因就是組件化開發(fā),一方面分模塊的方式便于協(xié)同開發(fā),降低耦合,后期維護(hù)也輕松;另一方面使得一次開發(fā),多處復(fù)用成為現(xiàn)實(shí),甚至可以直接復(fù)用開源React組件。開發(fā)完一個(gè)組件后,我們需要插入DOM中,一般使用如下代碼

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

經(jīng)過babel轉(zhuǎn)碼后為

ReactDOM.render(
        React.createElement(
          'h1',   // type, DOM原生組件的type為string,React自定義組件type為Object
          null,   // config,會(huì)設(shè)置到ref,key,props中
          'Hello, world!'   // children,子組件.這兒為文本組件
        ),
        document.getElementById('example')
)

那么React底層是怎么將組件插入DOM中的呢。本文來詳細(xì)分析它的前因后果。

2 ReactMount._renderSubtreeIntoContainer()

ReactDOM.render()實(shí)際調(diào)用ReactMount.render(),接著調(diào)用到ReactMount._renderSubtreeIntoContainer().
這個(gè)調(diào)用鏈比較簡(jiǎn)單,不分析了。下面重點(diǎn)分析_renderSubtreeIntoContainer(). 我們?nèi)コ糸_發(fā)調(diào)試階段的報(bào)錯(cuò)代碼(比如 “development” !== ‘production’)。

/**
   * 將ReactElement插入DOM中,并返回ReactElement對(duì)應(yīng)的ReactComponent。
   * ReactElement是React元素在內(nèi)存中的表示形式,可以理解為一個(gè)數(shù)據(jù)類,包含type,key,refs,props等成員變量
   * ReactComponent是React元素的操作類,包含mountComponent(), updateComponent()等很多操作組件的方法
   *
   * @param {parentComponent} 父組件,對(duì)于第一次渲染,為null
   * @param {nextElement} 要插入到DOM中的組件,對(duì)應(yīng)上面例子中的<h1>Hello, world!</h1>經(jīng)過babel轉(zhuǎn)譯后的元素
   * @param {container} 要插入到的容器,對(duì)應(yīng)上面例子中的document.getElementById('example')獲取的DOM對(duì)象
   * @param {callback} 第一次渲染為null
   *
   * @return {component}  返回ReactComponent,對(duì)于ReactDOM.render()調(diào)用,不用管返回值。
   */ 
_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
    // 剛開始一段開發(fā)階段的報(bào)錯(cuò)代碼,省去
    ...

    // 包裝ReactElement,將nextElement掛載到wrapper的props屬性下,這段代碼不是很關(guān)鍵
    var nextWrappedElement = ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
    // 獲取要插入到的容器的前一次的ReactComponent,這是為了做DOM diff
    // 對(duì)于ReactDOM.render()調(diào)用,prevComponent為null
    var prevComponent = getTopLevelWrapperInContainer(container);

    if (prevComponent) {
      // 從prevComponent中獲取到prevElement這個(gè)數(shù)據(jù)對(duì)象。一定要搞清楚ReactElement和ReactComponent的作用,他們很關(guān)鍵
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props;
      // DOM diff精髓,同一層級(jí)內(nèi),type和key不變時(shí),只用update就行。否則先unmount組件再mount組件
      // 這是React為了避免遞歸太深,而做的DOM diff前提假設(shè)。它只對(duì)同一DOM層級(jí),type相同,key(如果有)相同的組件做DOM diff,否則不用比較,直接先unmount再mount。這個(gè)假設(shè)使得diff算法復(fù)雜度從O(n^3)降低為O(n).
      // shouldUpdateReactComponent源碼請(qǐng)看后面的分析
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance();
        var updatedCallback = callback && function () {
          callback.call(publicInst);
        };
        // 只需要update,調(diào)用_updateRootComponent,然后直接return了
        ReactMount._updateRootComponent(prevComponent, nextWrappedElement, container, updatedCallback);
        return publicInst;
      } else {
        // 不做update,直接先卸載再掛載。即unmountComponent,再mountComponent。mountComponent在后面代碼中進(jìn)行
        ReactMount.unmountComponentAtNode(container);
      }
    }

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

    var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
    // 初始化,渲染組件,然后插入到DOM中。_renderNewRootComponent很關(guān)鍵,后面詳細(xì)分析
    var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, parentComponent != null ? parentComponent._reactInternalInstance._processChildContext(parentComponent._reactInternalInstance._context) : emptyObject)._renderedComponent.getPublicInstance();
    // render方法中帶入的回調(diào),ReactDOM.render()調(diào)用時(shí)一般不傳入
    if (callback) {
      callback.call(component);
    }
    return component;
  },

shouldUpdateReactComponent()源碼如下:

function shouldUpdateReactComponent(prevElement, nextElement) {
  // 前后兩次ReactElement中任何一個(gè)為null,則必須另一個(gè)為null才返回true。這種情況一般不會(huì)碰到
  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') {
    // 如果前后兩次為數(shù)字或者字符,則認(rèn)為只需要update(處理文本元素),返回true
    return (nextType === 'string' || nextType === 'number');
  } else {
      // 如果前后兩次為DOM元素或React元素,則必須type和key不變(key用于listView等組件,很多時(shí)候我們沒有設(shè)置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,根據(jù)ReactElement中不同的type字段,創(chuàng)建不同類型的組件對(duì)象,即ReactComponent
    // 前一篇文章中已經(jīng)分析了。http://blog.csdn.net/u013510838/article/details/55669769
    var componentInstance = instantiateReactComponent(nextElement);

    // 處理batchedMountComponentIntoNode方法調(diào)用,將ReactComponent插入DOM中,后面詳細(xì)分析
    ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);

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

    return componentInstance;
  },

batchedMountComponentIntoNode以transaction事務(wù)的形式調(diào)用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);
  }

  // 調(diào)用對(duì)應(yīng)ReactComponent中的mountComponent方法來渲染組件,這個(gè)是React生命周期的重要方法。后面詳細(xì)分析。
  // mountComponent返回React組件解析的HTML。不同的ReactComponent的mountComponent策略不同,可以看做多態(tài)
  // 上面的<h1>Hello, world!</h1>, 對(duì)應(yīng)的是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) {
    // 對(duì)于ReactDOM.render()調(diào)用,shouldReuseMarkup為false
    if (shouldReuseMarkup) {
      ...
    }

    if (transaction.useCreateElement) {
      // 清空container的子節(jié)點(diǎn),這個(gè)地方不明白為什么這么做
      while (container.lastChild) {
        container.removeChild(container.lastChild);
      }
      DOMLazyTree.insertTreeBefore(container, markup, null);
    } else {
      // 將markup這個(gè)HTML設(shè)置到container這個(gè)DOM元素的innerHTML屬性上,這樣就插入到了DOM中了
      setInnerHTML(container, markup);
      // 將instance這個(gè)ReactComponent渲染后的對(duì)象,即Virtual DOM,保存到container這個(gè)DOM元素的firstChild這個(gè)原生節(jié)點(diǎn)上。簡(jiǎn)單理解就是將Virtual DOM保存到內(nèi)存中,這樣可以大大提高交互效率
      ReactDOMComponentTree.precacheNode(instance, container.firstChild);
    }
  }

4 總結(jié)

ReactDOM.render()是渲染React組件并插入到DOM中的入口方法,它的執(zhí)行流程大概為

1.React.createElement(),創(chuàng)建ReactElement對(duì)象。?他的重要的成員變量有type,key,ref,props。這個(gè)過程中會(huì)調(diào)用getInitialState(), 初始化state,只在掛載的時(shí)候才調(diào)用。后面update時(shí)不再調(diào)用了。

2.instantiateReactComponent(),根據(jù)ReactElement的type分別創(chuàng)建ReactDOMComponent, ReactCompositeComponent,ReactDOMTextComponent等對(duì)象

3.mountComponent(), 調(diào)用React生命周期方法解析組件,得到它的HTML。

4._mountImageIntoNode(), 將HTML插入到DOM父節(jié)點(diǎn)中,通過設(shè)置DOM父節(jié)點(diǎn)的innerHTML屬性。

5.緩存節(jié)點(diǎn)在React中的對(duì)應(yīng)對(duì)象,即Virtual DOM。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。