图解 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 组件类。
  • 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。ui

  • 对于原来的旧 API React.createClass,如今推荐开发者用 class 的方式继承 Component 或者 PureComponent
  • 对于 PropTypes 的引入方式也不是原来的 import { PropTypes } from 'react',而变成了 import PropTypes from 'prop-types'

其余属性和方法咱们暂且就不详细的讲述了,这篇文章就只详细的研究一下和建立 React Virtual DOM 最紧密相关的方法——React.createElementthis

React.createElement 方法实际上是调用的ReactElement模块的 ReactElement.createElement 方法。spa

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前端高效开发实战》已在亚马逊、京东、当当开售


2019年,iKcamp原创新书《Koa与Node.js开发实战》已在京东、天猫、亚马逊、当当开售啦!

相关文章
相关标签/搜索