React Virtual DOM的理解(一)

835 阅读5分钟

js

React Virtual DOM

使用过react开发的人员一定听过react的Virtual DOM,那Virtual DOM到底是什么呢?接下我们来一探究竟。

React 基于16.13.0版本讲解

首先我们先来打印下React是什么

react

我们再来看一下源码的定义

react

var React = {
  // 子元素
  Children: {
    map: mapChildren, // 遍历子元素
    forEach: forEachChildren, // 类似map,但没有返回
    count: countChildren, // 返回 children 当中的组件总数
    toArray: toArray, // 将Children转换为数组
    only: onlyChild, 返回 children 中 仅有的子级。否则抛出异常。
  },
  Component, // 用来创建 React 组件类。
  Fragment, // 空标签,可带有key属性
  Profiler, // 测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”(生产环境中禁用)。
  PureComponent, // 用来创建 React 纯组件类。
  StrictMode, // 严格模式
  Suspense, // 可以使包裹的组件做优雅降级(如 loading 指示器等)。
  cloneElement, // 拷贝 React 元素。
  createContext, // 创建Context。
  createElement, //  创建 React 元素。
  createFactory, // 创建 React 工厂函数。(不建议使用)。
  createRef, // 创建ref
  forwardRef, // 转发ref
  isValidElement, // 判断是否是有效的 React 元素。
  lazy, // 懒加载,配合webpack使用会让你爱上它。
  memo, // React.PureComponent类似,用于一个函数组件而非一个类组件。
  // 以下都是16.8以后的新特性Hooks
  useCallback,
  useContext,
  useDebugValue,
  useEffect,
  useImperativeHandle,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
  version,
};

从控制台和源码都可以看到React是一个对象。

我们再来打印下<h1>hello world!

react

我们再打印出<App />,App的结构如下:

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

react

可以很直观的发现,打印的 HTML 元素并不是真实的 DOM 元素,打印的组件也不是 DOM 元素的集合,所有打印出来的元素都是一个对象,而且它们长的非常相似,那其实这些对象都是 React Element 对象。

var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    $$typeof: // REACT_ELEMENT_TYPE, React Element 的标志,是一个Symbol类型。
    type: type, // React 元素的类型。
    key: key, // React 元素的 key,diff 算法会用到。
    ref: ref, // React 元素的 ref 属性,当 React 元素生成实际 DOM 后,返回 DOM 的引用。
    props: props, // React 元素的属性,是一个对象。
    _owner: owner // 负责创建这个 React 元素的组件。
  };
  ...
  return element;
};

function createElement(type, config, children) {
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}

function createElementWithValidation(type, props, children) {
  var element = createElement.apply(this, arguments);
  return element;
}

参数中的 self 和 source 都是只供开发环境下用的参数。从上面的例子我们可以发现唯一不同的就是type 了,对于原生元素,type 是一个字符串类型,记录了原生元素的类型;对于 react 组件来说呢,type 是一个构造函数,或者说它是一个类,记录了这个 react 组件的是哪一个类的实例。所以 <App/>.type === App 的。

所以,每一个包装过后的React元素都是这样的对象

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

我们再来看一下 createElement 是怎么实现的:

function createElement(type, config, children) {
  var propName;
  var props = {};
  var key = null;
  var ref = null;
  var self = null;
  var source = null;

  // 初始化参数
  if (config != null) {
    // 初始化ref
    if (hasValidRef(config)) {
      ref = config.ref;
      {
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    // 初始化key
    if (hasValidKey(config)) {
      key = '' + config.key;
    }
    // 初始化self
    self = config.__self === undefined ? null : config.__self;
    // 初始化source
    source = config.__source === undefined ? null : config.__source;
    // 所有参数加入props这个对象
    for (propName in config) {
      if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    }
  }
  // 初始化Children
  var childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children; // 只有一个子元素,children成为第三个参数
  } else if (childrenLength > 1) {
    // 有多个子元素,所有子元素合并为数组,children以数组的形式成为第三个参数
    var childArray = Array(childrenLength);
    for (var i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }
  // 初始化defaultProps
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  {
    if (key || ref) {
      var displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;
      // 定义key的警告
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      // 定义ref的警告
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
  • 第一件是初始化 React Element 里的各种参数,例如 type,props 和 children 等。在初始化的时候,会提取出 key,ref 这两个属性,然后 __self,__source 这两个属性也是仅开发用。所以如果你在组件里定义了 key,ref,__self,__source 这4个属性中的任何一个,都是不能在 this.props 里访问到的。从第三个参数开始,传入的参数都会合并为 children 属性,如果只有一个,那么 children 就是第三个元素,如果超过一个,那么这些元素就会合并成一个 children 数组。

  • 第二件是初始化 defaultProps,我们可以发现,defaultProps 是通过 type 来初始化的,我们在上面也说过,对于 react 组件来说,type 是 React Element 所属的类,所以可以通过 type 取到该类的 defaultProps(默认属性)。这里还有一点需要注意,如果我们把某个属性的值定义成 undefined,那么这个属性也会使用默认属性,但是定义成 null 就不会使用默认属性。

react

创建Virtual DOM树

说了这么多,终于到重点了,先来看一下组件结构

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

需要注意的是,这些元素并不是真实的 DOM 元素, 它们只是一些对象,而且我们可以看到 React 组件实际上是概念上的形态,最终还是会生成原生的虚拟 DOM 对象。当这些对象上的数据发生变化时,会把变化同步到真实的 DOM 上去。 目前我们可以认为 Virtual DOM 就是这样的一种形态,但是实际上,并没有这么简单,这只是最基本的样子。

待更...

好了,今天先说到这里吧,欢迎吐槽。