React原始碼之元件的實現與首次渲染

whosmeya發表於2020-07-02

react: v15.0.0

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


babel 的編譯

babel 將 React JSX 編譯成 JavaScript.

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

React原始碼之元件的實現與首次渲染

每個標籤的建立都呼叫了 React.createElement.


原始碼中的兩種資料結構

貫穿原始碼,常見的兩種資料結構,有助於快速閱讀原始碼。

ReactElement

React原始碼之元件的實現與首次渲染

結構如下:

{
  $$typeof  // ReactElement識別符號
  type      // 元件
  key
  ref
  props     // 元件屬性和children
}

是 React.createElement 的返回值。

ReactComponent

ReactComponent 這個名字有點奇怪。

React原始碼之元件的實現與首次渲染

結構如下:

{
  _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。

React原始碼之元件的實現與首次渲染

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

React原始碼之元件的實現與首次渲染

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

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 原始碼的執行過程

React原始碼之元件的實現與首次渲染

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

相關文章