圖解 React Virtual DOM

做者: 阿希 (滬江Web前端開發工程師)
本文原創,轉載請註明做者及出處。html

瞭解 React 的人幾乎都聽過說 Virtual DOM,甚至不瞭解 React 的人也聽過 Virtual DOM。那麼 React 的 Virtual DOM 到底長什麼樣子呢?今天咱們將一探 React 的源碼來揭開 React Virtual DOM 的神祕面紗。前端

參考源碼爲React穩定版,版本號v15.4.1。react

1. React

咱們首先試着在控制檯打印一下 React 看看會是什麼樣子:算法

從控制檯看來,React是一個對象,那接下來咱們找到相應的源碼來確認看看(src/isomorphic/React.js):數組

var React = {
  Children: {
    map: ReactChildren.map,
    forEach: ReactChildren.forEach,
    count: ReactChildren.count,
    toArray: ReactChildren.toArray,
    only: onlyChild,
  },
  Component: ReactComponent,
  PureComponent: ReactPureComponent,
  createElement: createElement,
  cloneElement: cloneElement,
  isValidElement: ReactElement.isValidElement,
  PropTypes: ReactPropTypes,
  createClass: ReactClass.createClass,
  createFactory: createFactory,
  createMixin: function(mixin) {
    return mixin;
  },
  DOM: ReactDOMFactories,
  version: ReactVersion,
  __spread: __spread,
};

能夠了解到,React 確實是一個 Object ,咱們能夠把 React 對象畫成下圖的形式,方便你們直觀的觀察:babel

React 是一個對象,裏面包含了許多方法和屬性,有最新的 v15 版本的方法,也有些之前的 API 和一些已經廢棄不建議使用的 API。函數

  • Component 用來建立 React 組件類。this

  • PureComponent 用來建立 React 純組件類。spa

  • createElement 建立 React 元素。code

  • cloneElement 拷貝 React 元素。

  • isValidElement 判斷是不是有效的 React 元素。

  • PropTypes 定義 React props 類型。(過期的API)

  • createClass 建立 React 組件類(過期的API)。

  • createFactory 建立 React 工廠函數。(不建議使用)。

  • createMixin 建立 Mixin。

  • DOM 主要和同構相關。

  • version 當前使用的 React 版本號。

  • __spread 已廢棄,直接用 Object.assign() 代替

__spread 方法已經廢棄,再也不建議使用。在做者寫這篇文章的時候,React 又發佈了 v15.5.0 版本,在這個版本里,createClassPropTypes 也已經被標記爲過期的 API,會提示 warning。

  • 對於原來的舊 API React.createClass,如今推薦開發者用 class 的方式繼承 Component 或者 PureComponent

  • 對於 PropTypes 的引入方式也不是原來的 import { PropTypes } from 'react',而變成了 import PropTypes from 'prop-types'

其餘屬性和方法咱們暫且就不詳細的講述了,這篇文章就只詳細的研究一下和建立 React Virtual DOM 最緊密相關的方法——React.createElement

React.createElement 方法實際上是調用的ReactElement模塊的 ReactElement.createElement 方法。

2. React Element

Virtual DOM 是真實 DOM 的模擬,真實 DOM 是由真實的 DOM 元素構成,Virtual DOM 也是由虛擬的 DOM 元素構成。真實 DOM 元素咱們已經很熟悉了,它們都是 HTML 元素(HTML Element)。那虛擬 DOM 元素是什麼呢?React 給虛擬 DOM 元素取名叫 React 元素(React Element)。

咱們知道,React 能夠經過組合一些 HTML 原生元素造成組件,而後組件又能夠被其餘的組件複用。因此,原生元素和組件其實在概念上都是一致的,都是具備特定功能和 UI 的可複用的元素。所以,React 把這些元素抽象成了 React Element。不管是 HTML 原生元素,例如:<p></p><a></a>,等。或者這些原生元素的組合(組件),例如 <Message /> 等。它們都是 React Element,而建立這些 Element 的方法就是 React.createElement

React Virtual DOM 就是由 React Element 構成的一棵樹

接下來咱們就探究下 React Element 到底長什麼樣以及 React 是如何建立這些 React Element 的。

2.1 ReactElement 模塊

咱們在控制檯裏直接打印出 <h1>hello</h1>

咱們再打印出 <App />,App 組件的結構以下:

<div>
    <h1>App</h1>
    <p>Hello world!</p>
</div>

打印出的結果以下:

能夠很直觀的發現,打印的 HTML 元素並非真實的 DOM 元素,打印的組件也不是 DOM 元素的集合,全部打印出來的元素都是一個對象,並且它們長的很是類似,那其實這些對象都是 React Element 對象。

而後咱們再看看源碼部分:

var ReactElement = function(type, key, ref, self, source, owner, props) {
  var element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };
  if (__DEV__) {
    // ...
  }
  return element;
};

ReactElement實際上是一個工廠函數,接受7個參數,最終返回一個React Element對象。

  • $$type React Element 的標誌,是一個Symbol類型。

  • type React 元素的類型。

  • key React 元素的 key,diff 算法會用到。

  • ref React 元素的 ref 屬性,當 React 元素生成實際 DOM 後,返回 DOM 的引用。

  • props React 元素的屬性,是一個對象。

  • _owner 負責建立這個 React 元素的組件。

參數中的 selfsource 都是隻供開發環境下用的參數。從上面的例子咱們能夠發現惟一不一樣的就是type 了,對於原生元素,type 是一個字符串類型,記錄了原生元素的類型;對於 react 組件來講呢,type 是一個構造函數,或者說它是一個類,記錄了這個 react 組件的是哪個類的實例。因此<App/>.type === App 的。

因此,每個包裝事後的React元素都是這樣的對象:

{
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
}

用圖片表示 React Element,就是下圖這樣:

2.2 ReactElement.createElement 方法

在此以前,可能有人會問,咱們開發當中彷佛沒有用到 React.createElement 方法呀。其實否則,看下面的示例:

class OriginalElement extends Component {
  render() {
    return (
      <div>Original Element div</div>
    );
  }
}

通過babel轉譯以後是這樣的

_createClass(OriginalElement, [{
    key: "render",
    value: function render() {
      return React.createElement(
        "div",
        null,
        "Original Element div"
      );
    }
  }]);

能夠看到,全部的 JSX 都會被編譯成 React.createElement 方法,因此這個方法多是咱們在使用React用的最多的方法。

接下來咱們看看 React.createElement 方法是怎樣的,前面說過了 React.createElement 方法其實就是 ReactElement.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) {
    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;

    for (propName in config) {
      if (hasOwnProperty.call(config, propName) &&
          !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    }
  }
  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];
    }
    if (__DEV__) {
      // ...
    }
    props.children = childArray;
  }
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  if (__DEV__) {
    // ...
  }
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props
  );
};

reactElement.createElement大體作了2件事。

第一件是初始化 React Element 裏的各類參數,例如 typepropschildren 等。在初始化的時候,會提取出 keyref 這兩個屬性,而後 __self,__source 這兩個屬性也是僅開發用。因此若是你在組件裏定義了 keyref__self__source 這4個屬性中的任何一個,都是不能在 this.props 裏訪問到的。從第三個參數開始,傳入的參數都會合併爲 children 屬性,若是隻有一個,那麼 children 就是第三個元素,若是超過一個,那麼這些元素就會合併成一個 children 數組。

第二件是初始化 defaultProps,咱們能夠發現,defaultProps 是經過 type 來初始化的,咱們在上面也說過,對於 react 組件來講,type 是 React Element 所屬的類,因此能夠經過 type 取到該類的 defaultProps(默認屬性)。這裏還有一點須要注意,若是咱們把某個屬性的值定義成 undefined,那麼這個屬性也會使用默認屬性,可是定義成 null 就不會使用默認屬性。

下面是圖解:

4. 建立Virtual DOM樹

有了上面的做爲基礎,那建立 Virtual DOM 就很簡單了。整個 Virtual DOM 就是一個巨大的對象。

好比咱們有這麼一個 App

App:
<div>
  <Header />
  <List />
</div>

Header:
<div>
  <Logo />
  <button>菜單</button>
</div>

List:
<ul>
  <li>text 1</li>
  <li>text 2</li>
  <li>text 3</li>
</ul>

Logo:
<div>
  <img src="./foo.png" alt="logo" />
  <p>text logo</p>
</div>

ReactDOM.render(<App />, document.getElementById('root'))

經過上面的瞭解到的 React Element 建立方式,咱們不難知道,生成的對應的 Virtual DOM 應該是相似於這樣的:

須要注意的是,這些元素並非真實的 DOM 元素, 它們只是一些對象,並且咱們能夠看到 React 組件其實是概念上的形態,最終仍是會生成原生的虛擬 DOM 對象。當這些對象上的數據發生變化時,經過打 patch 把變化同步到真實的 DOM 上去。

目前咱們能夠認爲 Virtual DOM 就是這樣的一種形態,可是實際上,並無這麼簡單,這只是最基本的樣子,在後續的文章中我會帶你們一塊兒看看更高級的形態。


圖片描述

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

相關文章
相關標籤/搜索