React中的Virtual DOM是什么

5,335 阅读6分钟

在讲解虚拟DOM(Virtual DOM)之前可以先了解下真实的DOM是什么

Virtual DOM

Virtual DOM 本质上是JavaScript对象,是对真实DOM的的一种描述方式。
即JS对象模拟的DOM结构,将DOM变化的对比放在JS层来做。让UI渲染变成 UI = f(data)的形式。
示例:HTML里的一段标签文本

<ul id="list">
    <li class="item">Item1</li>
    <li class="item">Item2</li>
</ul>

用虚拟DOM结构可以表示为:

{
    tag: "ul",
    attrs: {
        id: "list"
    },
    children: [
        {
            tag: "li",
            attrs: { className: "item" },
            children: ["Item1"]
        }, {
            tag: "li",
            attrs: { className: "item" },
            children: ["Item2"]
        }
    ]
} 

tag是标签名称
attrs是标签属性组成的对象
children是包含在标签内的子标签

所以,Virtual DOM 其实是一个JS对象,类似JSON格式,React中就是将JSX元素的名称、属性和内容作为对象及其属性来创建Virtual DOM。

为什么需要Virtual DOM

  • 跨平台。支持跨平台,可以将JS对象渲染到浏览器DOM以外的环境中,通过Virtual DOM可以实现服务器端渲染。比如在Node.js中没有DOM,可以借助Virtual DOM 来实现SSR。支持了跨平台开发,比如ReactNative。
  • 性能优化。前端性能优化的一个秘诀就是尽可能少地操作DOM,优秀的Virtual DOM Diff 算法使得Virtual DOM将DOM的比对操作放在JS层,实现的在patch过程中尽可能地一次性将差异更新到DOM中,减少浏览器不必要的重绘,提高效率。这样保证了DOM不会出现性能很差的情况。
  • 开发体验。我们更新网上的展示内容是通过操纵HTML元素来实现的,要先获取到元素,然后再更新。虽然有jQuery框架来实现,但是还是比较复杂的。React框架用Virtual DOM帮助开发者不需要面对复杂的api,声明式编写UI,专注于更改数据就行了。

不过,针对单一场景的性能优化,专门针对该场景的js进行极致优化 比 Virtual DOM 方案更适用。

React中的Virtual DOM

Virtual DOM 元素

在React开发中,Virtual DOM 元素即是React 元素,经常会使用JSX来写React元素,比如
简单的

<div>Hello</div>

稍微复杂一些的

<div className="divStyleName" onClick={(e) => console.log('点击了')}>
    Hello
    <img src="pic_url">img</img>
    <div>div2</div>
    <div></div>
</div>;

不过,在React中不是必须要使用JSX的。
因为其实 JSX 元素只是调用 React.createElement(component, props, ...children) 的语法糖。其本质还是JavaScript,所有的JSX语法最终都会被转换成对这个方法的调用。因此,使用 JSX 可以完成的任何事情都可以通过纯 JavaScript 完成。
同时,浏览器是不能直接读取JSX,为了使浏览器能够读取JSX,需要在项目构建环境中配置有关 JSX 编译。比如 让 Babel 这样的 JSX 转换器将 JSX语句 转换为 目标 js 代码,再将其传给浏览器,JS引擎才能成功执行语句。

用纯JavaScript来写上面的JSX示例(经过babel转译)

React.createElement("div", {
    className: "divStyleName",
    onClick: e => console.log('点击了')
  }, "Hello", React.createElement("img", {
    src: "pic_url"
  }, "img"), React.createElement("div", null, "div2"), React.createElement("div", null));

从最层的React.createElement分析起
第一个参数值是组件名
第二个参数值是一个对象,由组件的属性构成
第三到后面的一系列参数值,就是子元素了。如果子元素依然是JSX元素,就继续通过React.createElement来创建对象
React.createElement创建的React元素就是虚拟DOM树的元素,于是,就构成出一个虚拟DOM树了。

这里在浏览器窗口中看下上面所述内容的结果

注意这里的type的值是"div",是同名的原生 html 的div标签名

再来自定义一个组件,名为HelloComponent,JSX实现为

class HelloComponent extends React.Component {
  render() {
    return <div>Hello {this.props.toWhat}</div>;
  }
}

<HelloComponent toWhat="World" />

Babel转译后对应的js代码为

class HelloComponent extends React.Component {
  render() {
    return React.createElement("div", null, "Hello ", this.props.toWhat);
  }
}

React.createElement(HelloComponent, {
    toWhat: "World"
  });

然后在浏览器窗口看下

注意这里type的值是一个类构造函数了

这是babel在编译时会判断JSX中组件的首字母:

  • 当首字母为小写时,其被认定为同名的原生 html标签,createElement的第一个变量被编译为字符串
  • 当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象。

所以自定义组件名称必须要首字母大写。

下面就来研究下React.createElement(component, props, ...children)具体的实现 以及 是如何来构建Virtual DOM的吧~

Virtual DOM 实现原理

要开始看React 源码啦~

ReactElement

因为是React.createElement,所以自然就找到React.js文件,查看其createElement方法。 然后就发现其实的调用了ReactElement.js文件中的createElement方法,返回一个ReactElement对象

/**
 * Create and return a new ReactElement of the given type.
 * See https://reactjs.org/docs/react-api.html#createelement
 */
export function createElement(type, config, children) {
  let propName;

  // Reserved names are extracted
  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  /* 
  ...
  根据入参各种逻辑判断赋值ref、source、self、props等
  ...
  */
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

再看下ReactElement方法的具体实现

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    ?typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  if (__DEV__) {
    // 自然是给element的各个属性赋值了
  }

  return element;
};

返回的是是一个JS对象,在React中称为ReactElement对象:

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

结构正是上面在浏览器窗口中看到的React.createElement的结果的结构。

这里简单介绍下ReactElement对象的属性及其含义。

  • <text>?typeof: React 元素的标识,即"react.element",Symbol类型。
  • type: React 元素的类型,前面看到值可以是字符串,也可以是类构造函数。
  • props: React 元素的属性,是一个对象,由JSX的属性值和子元素构成。
  • key: React 元素的 key,Virtual DOM Diff 算法里会用到。
  • ref: React 元素的 ref 属性,当 React 元素生成真实的 DOM 元素后,返回 DOM 元素的引用。
  • _owner: 负责创建这个 React 元素的组件。

创建Virtual DOM树

了解了单个ReactElement对象后,前面提到的示例层层调用React.createElement,就可以创建出一个Virtual DOM Tree了,即生成原生的虚拟 DOM 对象。

React.createElement("div", {
    className: "divStyleName",
    onClick: e => console.log('点击了')
  }, "Hello", React.createElement("img", {
    src: "pic_url"
  }, "img"), React.createElement("div", null, "div2"), React.createElement("div", null));

参考资料

在线Babel编辑器编写查看更多JSX转换成JavaScript的示例

图解 React Virtual DOM

React 源码