2-4 虚拟 DOM

Q: 什么是虚拟 DOM ? 其优点是什么?

虚拟 DOM 最早是由 React 团队提出来的,因此 React 团队在对虚拟 DOM 的定义上有绝对的话语权。Virtual DOM 是一种编程概念。在这个概念里,UI 以一种理想化,或者是“虚拟”的表现形式被保存在内存中。也就是说,只要我们能够通过一种方式,将真实的 DOM 层次结构描述出来,那么这就是一个虚拟 DOM。

image-20230223114000816

在 React 中,React 团队使用的是 JS 对象对 DOM 结构进行一个描述。但是很多人会把 JS 对象和虚拟 DOM 划等号,这种理解是不准确的,也是片面的。虚拟 DOM 和 JS 对象之间的关系:前者是一种思想,后者是一种思想的具体体现

为什么需要虚拟 DOM

使用虚拟 DOM 主要有两个方面的优势:1、相较于真实 DOM 的体积优势和速度优势2、多平台渲染的抽象能力

相较于真实 DOM 的体积优势和速度优势

首先我们需要明确一个点,JS 层面的计算速度要比 DOM 层面的计算要快,主要原因有两点:一个是,DOM 对象最终要被浏览器渲染出来之前,浏览器要有很多工作要做(可以浏览器的渲染原理);另外一个原因是 DOM 对象上的属性也非常的多。

const div = document.createElement("div");
const props = [];
for (let i in div) {
  props.push(i);
}
console.log(props); // 340+属性

上面的代码在浏览器控制台运行后,能够看到 div 元素上所有的 DOM 属性。且操作 JS 对象的时间和操作 DOM 对象的时间是完全不一样的。JS 层面的计算速度要高于 DOM 层面的计算速速。此时有一个问题:虽然使用了 JS 对象来描述 UI,但是最终不还是要用原生 DOM API 去操作 DOM 吗?

虚拟 DOM 在第一次渲染页面的时候,并没有什么优势,速度肯定要比直接操作原生 DOM API 要慢一些,虚拟 DOM 真正体现优势是在更新阶段。根据 React 团队的研究,在渲染页面时,相比使用原生 DOM API,开发人员更加倾向于使用 innerHTML。

let newP = document.createElement("p");
let newContent = document.createTextNode("this is a test");
newP.appendChild(newContent);
document.body.appendChild(newP);
document.body.innerHTML = `
<p>
this is a test
</p>
`;

因此在使用 innerHTML 的时候,就涉及到两个层面的计算:

  • JS 层面:解析字符串;
  • DOM 层面: 创建对应的 DOM 节点;

接下来我们加入虚拟 DOM 来进行对比。

innerHTML 虚拟 DOM
JS 层面计算 解析字符串 创建 JS 对象
DOM 层面计算 创建对应的 DOM 节点 创建对应的 DOM 节点

虚拟 DOM 真正发力的时候,是在更新阶段。innerHTML 进行更新的时候,要全部重新赋值。这意味着之前创建的 DOM节点要全部销毁掉,然后重新进行创建
。但是虚拟 DOM 只需要更新必要的 DOM 节点即可。

下面是更新阶段的对比。

innerHTML 虚拟 DOM
JS 层面计算 解析字符串 创建 JS 对象
DOM 层面计算 销毁原来所有的 DOM 节点 修改必要的 DOM 节点
DOM 层面计算 创建对应的 DOM 节点
多平台的渲染抽象能力

UI=f(state),这个公式可以拆分成两步:

  • 根据自变量的变化计算出 UI;
  • 根据 UI 的变化执行具体的宿主环境的 API

虚拟 DOM 只是对真实 UI 的一个描述,根据不同的宿主环境,可以执行不同的渲染代码。

  • 浏览器、Node.js 宿主环境使用 ReactDOM 包;
  • Native 宿主环境使用 ReactNative 包;
  • Canvas、SVG 或者 VML(IE8) 宿主环境使用 ReactArt 包;
  • ReactTest 包用于渲染 JS 对象,可以很方便的测试“不隶属于任何宿主环境的通用功能”。

React 中的虚拟 DOM

在 React 中通过 JSX 来描述 UI, JSX 最终会被转化为一个叫做 createElement 方法的调用,调用该方法后就会得到虚拟 DOM 对象。经过 Babel 编译后的结果如下:

在源码中 createElement 方法如下:

/**
 * <h1 id="aa"> hello </h1>
 * @param {*} type 元素类型  h1
 * @param {*} config 属性对象 {id: "aa"}
 * @param {*} children 子元素 hello
 * @returns 一个虚拟 DOM 对象
 */
export function createElement(type, config, children) {
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;
  // 说明有属性
  if (config !== null) {
    //...
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }
  // 经历了上面的 if 之后,所有的属性都被放到了 props 中
  // props ----> { id: 'aa' }

  // children 可以有多个参数,这些参数被转义到新分配的 props 对象上
  // 如果是多个子元素,对应的是一个数组
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
    // 如果有多个子元素,那么 children 是一个数组
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    //...
    props.children = childArray;
  }

  // 添加默认的 props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  //...

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

const ReactElement = function (type, key, ref, self, source, owner, props) {
  // 该对象是最终向外部返回的 vdom(也就是用来描述 DOM 层次结构的 JS 对象)
  const element = {
    // 标识当前对象是一个 ReactElement
    $$typeof: REACT_ELEMENT_TYPE,

    // 元素类型
    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录当前元素的创建者
    _owner: owner,
  };
  //...
  return element;
};

在上面的代码中,最终返回的 element 对象就是我们所说的虚拟 DOM 对象。在官方文档中,更倾向于称之为 React 元素。

最后总结,如何回答什么是虚拟 DOM?其优点有哪些?

虚拟 DOM 最初是由 React 团队提出的概念,这是一种编程的思想,指的是针对真实 UI DOM 的一种描述能力。在 React 中,使用了 JS 对象来描述真实的 DOM 结构。虚拟 DOM 和 JS 对象之间的关系:前者是一种思想,后者是这种思想的具体体现。使用虚拟 DOM 有如下的优点:

  • 相较于真实 DOM 的体积和速度优势
  • 多平台渲染的抽象能力

相较于真实 DOM 的体积和速度优势, JS 层面的计算速度,要比 DOM 层面的计算速度快得多。 DOM 对象最终要被浏览器显示出来之前,浏览器会有很多的工作要做(浏览器渲染原理);DOM 上的属性也是非常多的。虚拟 DOM 发挥优势的时机主要体现在更新的时候,相比较 innerHTML 的方式,将已有的 DOM 节点全部销毁后再重新创建,虚拟 DOM 能够做到针对 DOM 节点做最小程度的修改。

多平台的渲染抽象能力,指的是虚拟 DOM 可以在不同的宿主环境中执行渲染。

  • 浏览器、Node.js 宿主环境使用 ReactDOM 包;
  • Native 宿主环境使用 ReactNative 包;
  • Canvas、SVG 或者 VML(IE8) 宿主环境使用 ReactArt 包;
  • ReactTest 包用于渲染 JS 对象,可以很方便的测试“不隶属于任何宿主环境的通用功能”。

在 React 中通过 JSX 来描述 UI, JSX 仅仅是一个语法糖,会被 Babel 编译为 createElement 方法的调用,该方法调用之后会返回一个 JS 对象,也就是虚拟 DOM 对象,官方更倾向于称之为 React 元素。