React源碼之組件的實現與首次渲染

react: v15.0.0

本文講 組件如何編譯 以及 ReactDOM.render 的渲染過程。


babel 的編譯

babel 將 React JSX 編譯成 JavaScript.

在 babel 官網寫一段 JSX 代碼編譯結果如圖:

每個標籤的創建都調用了 React.createElement.


源碼中的兩種數據結構

貫穿源碼,常見的兩種數據結構,有助於快速閱讀源碼。

ReactElement

結構如下:

{
  $$typeof  // ReactElement標識符
  type      // 組件
  key
  ref
  props     // 組件屬性和children
}

是 React.createElement 的返回值。

ReactComponent

ReactComponent 這個名字有點奇怪。

結構如下:

{
  _currentElement    // ReactElement
  ...

  // 原型鏈上的方法
  mountComponent,    // 組件初次加載調用
  updateComponent,   // 組件更新調用
  unmountComponent,  // 組件卸載調用
}

是 ReactCompositeComponent 的 instance 類型。其餘三種構造函數 ReactDOMComponent、ReactDOMTextComponent、ReactEmptyComponent 的實例結構與其相似。


React.createElement

React.createElement 實際執行的是 ReactElement.createElement。

ReactElement.createElement 接收三個參數, 返回 ReactElement 結構。

  • type: string | Component
  • config: 標籤上的屬性
  • ...children: children元素集合

重點關注 type 和 props。

然後看 ReactElement 方法,只是做了賦值動作。

綜上,我們寫的代碼編譯後是這樣的:

class C extends React.Component {
  render() {
    return {
      type: "div",
      props: {
        children: this.props.value,
      },
    };
  }
}

class App extends React.Component {
  render() {
    return {
      type: "div",
      props: {
        children: [
          {
            type: "span",
            props: {
              children: "aaapppppp",
            },
          },
          "123",
          {
            type: C,
            props: {
              value: "ccc",
            },
          },
        ]
      },
    };
  }
}

ReactDOM.render(
  {
    type: App,
    props: {},
  },
  document.getElementById("root")
);

ReactDOM.render

先來看下 ReactDOM.render 源碼的執行過程


instantiateReactComponent

在 _renderNewRootComponent 方法中,調用了 instantiateReactComponent,生成了的實例結構類似於 ReactComponent。

instantiateReactComponent 的參數是 node,node 的其中一種格式就是 ReactElement。

根據 node & node.type 的類型,會執行不同的方法生成實例

  • ReactCompositeComponent
  • ReactDOMComponent
  • ReactDOMTextComponent
  • ReactEmptyComponent

簡化如下

var instantiateReactComponent = function (node) {
  if (node === null || node === false) {
    return new ReactEmptyComponent(node);
  } else if (typeof node === 'object') {
    if (node.type === 'string') {
      return new ReactDOMComponent(node);
    } else {
      return new ReactCompositeComponent(node);
    }
  } else if (typeof node === 'string' || typeof node === 'number') {
    return new ReactDOMTextComponent(node);
  }
}

通過四種方式實例化後的對象基本相似

var instance = {
  _currentElement: node,
  _rootNodeID: null,
  ...
}
instance.__proto__ = {
  mountComponent,
  updateComponent,
  unmountComponent,
}

四種 mountComponent 簡化如下

ReactCompositeComponent

mountComponent: function () {
  // 創建當前組件的實例
  this._instance = new this._currentElement.type();

  // 調用組件的 render 方法,得到組件的 renderedElement
  renderedElement = this._instance.render();

  // 調用 instantiateReactComponent,  得到 renderedElement 的實例化 ReactComponent
  this._renderedComponent = instantiateReactComponent(renderedElement);

  // 調用 ReactComponent.mountComponent
  return this._renderedComponent.mountComponent();
}

ReactDOMComponent

react 源碼中,插入 container 前使用 ownerDocument、DOMLazyTree 創建和存放節點,此處爲了方便理解,使用 document.createElement 模擬。

mountComponent: function () {
  var { type, props } = this._currentElement;

  // 創建dom 源碼中使用 ownerDocument
  var element = document.createElement(type);

  // 遞歸children (源碼中使用 DOMLazyTree 存放 並返回)
  if (props.children) {
    var childrenMarkups = props.children.map(function (node) {
      var instance = instantiateReactComponent(node);
      return instance.mountComponent();
    })

    element.appendChild(childrenMarkups)
  }

  return element;
}

ReactDOMTextComponent

mountComponent: function () {
  return this._currentElement;
}

ReactEmptyComponent

mountComponent: function () {
  return null;
}

ReactDOM.render 簡化

簡化如下:

ReactDOM.render = function (nextElement, container) {
  // 添加殼子
  var nextWrappedElement = ReactElement(
    TopLevelWrapper,
    null,
    null,
    null,
    null,
    null,
    nextElement
  );

  // 實例化 ReactElement
  var componentInstance = instantiateReactComponent(nextElement);

  // 遞歸生成html
  var markup = componentInstance.mountComponent;

  // 插入真實dom
  container.innerHTML = markup;
}

總結

  1. babel 將 JSX 語法編譯成 React.createElement 形式。
  2. 源碼中用到了兩個重要的數據結構
    • ReactElement
    • ReactComponent
  3. React.createElement 將我們寫的組件處理成 ReactElement 結構。
  4. ReactDOM.render 傳入 ReactElement 和 container, 渲染流程如下
    • 在 ReactElement 外套一層,生成新的 ReactElement
    • 實例化 ReactElement:var instance = instantiateReactComponent(ReactElement)
    • 遞歸生成 markup:var markup = instance.mountComponent()
    • 將 markup 插入 container:container.innerHTML = markup

whosmeya.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章