圖解 React Virtual DOM

iKcamp發表於2017-08-30

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

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

參考原始碼為React穩定版,版本號v15.4.1。

1. React

我們首先試著在控制檯列印一下 React 看看會是什麼樣子:

圖解 React Virtual DOM

從控制檯看來,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 物件畫成下圖的形式,方便大家直觀的觀察:

圖解 React Virtual DOM

React 是一個物件,裡面包含了許多方法和屬性,有最新的 v15 版本的方法,也有些以前的 API 和一些已經廢棄不建議使用的 API。

  • Component 用來建立 React 元件類。
  • PureComponent 用來建立 React 純元件類。
  • createElement 建立 React 元素。
  • 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 Virtual DOM

我們知道,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>

圖解 React Virtual DOM

我們再列印出 <App />,App 元件的結構如下:

<div>
	<h1>App</h1>
	<p>Hello world!</p>
</div>
複製程式碼

列印出的結果如下:

圖解 React Virtual DOM

可以很直觀的發現,列印的 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,就是下圖這樣:

圖解 React Virtual DOM

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 就不會使用預設屬性。

下面是圖解:

圖解 React Virtual DOM

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 應該是類似於這樣的:

圖解 React Virtual DOM

需要注意的是,這些元素並不是真實的 DOM 元素, 它們只是一些物件,而且我們可以看到 React 元件實際上是概念上的形態,最終還是會生成原生的虛擬 DOM 物件。當這些物件上的資料發生變化時,通過打 patch 把變化同步到真實的 DOM 上去。

目前我們可以認為 Virtual DOM 就是這樣的一種形態,但是實際上,並沒有這麼簡單,這只是最基本的樣子,在後續的文章中我會帶大家一起看看更高階的形態。

圖解 React Virtual DOM

圖解 React Virtual DOM

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


圖解 React Virtual DOM

2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章