React渲染過程源碼分析

什麼是虛擬DOM(Virtual DOM)

在傳統的開發模式中,每次須要進行頁面更新的時候都須要咱們手動的更新DOM:html

在前端開發中,最應該避免的就是DOM的更新,由於DOM更新是極其耗費性能的,有過操做DOM經歷的都應該知道,修改DOM的代碼也很是冗長,也會致使項目代碼閱讀困難。在React中,把真是得DOM轉換成JavaScript對象樹,這就是咱們說的虛擬DOM,它並非真正的DOM,只是存有渲染真實DOM須要的屬性的對象。前端

虛擬DOM的好處

雖然虛擬DOM會提高必定得性能可是並不明顯,由於每次須要更新的時候Virtual DOM須要比較兩次的DOM有什麼不一樣,而後批量更新,這也是須要資源的。node

Virtual真實的好處實際上是,他能夠實現跨平臺,咱們所熟知的react-native就是基於VirtualDOM來實現的。react

Virtual DOM實現

如今咱們根據源碼來分析一下Virtual DOM的構建過程。json

JSX和React.createElementreact-native

在看源碼以前,如今回顧一下React中建立組件的兩種方式。緩存

1.JSX安全

function App() {
  return (
    <div>Hello React</div>
  );
}
複製代碼

2.React.createElementbash

const App = React.createElement('div', null, 'Hello React');
複製代碼

這裏多說一句其實JSX只不過是React.createElement的語法糖,在編譯的時候babel會將JSX轉換成爲使用React.createElement的形式,由於JSX語法更加符合咱們平常開發的習慣,因此咱們在寫React的時候更多的是使用JSX語法進行編寫。babel

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的結果:

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.返回組件
複製代碼

3.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建立了,組件的實例,其實組件類型有四種,具體看下圖:

在這裏咱們仍是看一下它的具體實現,而後分析一下過程:

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節點的吧。

4.生成DOM(mountComponentIntoNode)

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.

5.渲染DOM(_mountImageIntoNode)

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

相關文章
相關標籤/搜索