虚拟 DOM 这个概念,相信在接触过 React 或者 Vue 的同学一定都不陌生,但是在实际开发中,跟一些同学讨论问题时却发现同学们对 虚拟 DOM 的作用和 React 的渲染过程 的理解有些偏差。这里也给自己做个备忘吧。

要解释 虚拟DOM ,我们还要从 JSX 讲起。

JSX 是什么?

要讲 React ,肯定是绕不过 JSX ,JSX 是 React 灵魂所在。那 JSX 是什么?

是组件?那组件又是什么?

其实 官网 已经有了明确的说明,JSX 不过是一层语法糖,比如下面的 JSX 代码:

<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>

编译之后,就会变成:

React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

如果再复杂一点:

<div className="cn">
    <Header> Hello, This is React </Header>
    <div>Start to learn right now!</div>
    Right Reserve.
</div>

编译之后:

React.createElement(
        'div',
        { className: 'cn' },
        React.createElement(
            Header,
            null,
            'Hello, This is React'
        ),
        React.createElement(
            'div',
            null,
            'Start to learn right now!'
        ),
        'Right Reserve'
    )

如果你想测试一些特定的 JSX 会转换成什么样的 JavaScript,你可以尝试使用 在线的 Babel 编译器

在代码中也找到了对应的位置,官网上也有说到这一点。

我们可以看到 createElement 这个方法的入参只有三个,也就是刚才看到的编译出来的三个入参。最终会返回一个叫 ReactElement 的对象。

function createElement(type, config, children) {
  var propName; // Reserved names are extracted

  var props = {};
  var key = null;
  var ref = null;
  var self = null;
  var source = null;

  ...

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

我们再来看一下 ReactElement 是什么。

var ReactElement = function (type, key, ref, self, source, owner, props) {
  var 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
  };

  ...
  return element;
};

诶,最后返回的是一个对象, JSX 最后会变成一个对象?

是的!但是 Render 函数中,返回的不止有 ReactElement 还有一些其他的东西,主要有这下面几类:

  • React 元素。通常通过 JSX 创建。例如,<div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件,无论是 <div /> 还是 <MyComponent /> 均为 React 元素。
  • 数组或 fragments。 使得 render 方法可以返回多个元素,其实也就是我们平时使用的 <></>。欲了解更多详细信息,请参阅 fragments 文档。
  • Portals。可以渲染子节点到不同的 DOM 子树中。欲了解更多详细信息,请参阅有关 portals 的文档
  • 字符串或数值类型。它们在 DOM 中会被渲染为文本节点。
  • 布尔类型或 null。什么都不渲染。(主要用于支持返回 test && <Child /> 的模式,其中 test 为布尔类型。)

官网中也有说明:

虚拟 DOM 树

说完了 ReactElement ,我们就可以来说一下 虚拟 DOM 了。虚拟 DOM 树其实是 DOM 树的一个映射,通过 虚拟 DOM 树,React 可以还原出一颗完整 DOM 树,而 DOM 树也可以使用 虚拟 DOM 树来保存其结构。有点像 序列化 和 反序列化 的关系。

虚拟 DOM 节点 主要有这几类:

  • ReactDOMTextComponent:用来负责text node对应的虚拟 DOM
  • ReactDOMComponent:用来负责html标签对应的虚拟 DOM
  • ReactEmptyComponent : 用来负责 null ,false 的虚拟 DOM
  • ReactCompositeComponent:用来负责继承 React.Component 对应的虚拟 DOM,也就是自定义的 组件。

这几种类型涵盖了 Render 函数中返回的类型,

React 元素、数组或者Fragment、Portals,最后会解析为 ReactDOMComponent ,

字符串和数组类型,最后会解析为 ReactDOMTextComponent ,

布尔类型或 null 最后会解析为 ReactEmptyComponent。

也就是说我们所写的、所使用的 JSX 或者 组件 最后都会变成一个个的 虚拟 DOM,组合起来,最后会变成一个 虚拟 DOM 树。也就是我们常说的 虚拟 DOM 树。

那虚拟 DOM 节点又是一个什么东西?

其实也是一个 JS 对象 ,里面有 一堆的属性,和一些用来进行挂载、更新等渲染相关的方法。我们来看一下 15.0.0 版本的代码:

为什么要使用虚拟 DOM?

为什么要使用虚拟 DOM 这个问题,还要从 React 的开发模式说起。

文章看到这里,我想在座的各位应该都知道 React 是非常典型的 MVVM 设计,需要更新数据的时候,开发者只需要通过 setState 直接把数据塞给 React 就可以了,完全不用考虑数据的渲染。在这之前,传统的 Web前端开发,可能还在使用 JQ 或者手动去更新界面数据,过程比较繁琐还容易出错,操作不当还会有性能的问题,如果使用的是 MVVM 的这种开发框架,直接把数据一给,就完事了。就像如果你自己做饭,需要买菜、洗菜、切菜、做饭、洗碗,如果使用了 MVVM 的框架,就像是点外卖一样,只需要在下单,等外卖到就可以吃饭了,吃完还不用洗碗,你说爽不爽。

但是岁月静好的背后一定有人在为你负重前行,这个人就是 React 的渲染机制,其中最关键之一就是 虚拟DOM,React 的所有工作几乎都基于 虚拟DOM 完成的。

所以虚拟 DOM 有什么用?

最主要的作用就是提升性能。

其实就像 Android 的视图渲染一样,操作 DOM 也是一个比较耗性能的操作,如果频繁触发,就会导致性能问题,界面出现卡顿。虚拟 DOM 的存在,就是尽量减少对 DOM 的操作,从而提高性能。

这一步是怎么实现的?

再回到刚才讲的 虚拟 DOM 节点上,当 React 触发更新的时候,会依次调用 Render 函数,返回一个个的 虚拟DOM 节点,最后组装一颗新的虚拟 DOM 树,再跟旧的 虚拟 DOM 树 进行比较,得出需要更新的部分,然后再把这部分更新到 DOM 中。

其中跟性能相关有两个关键点,一个是批处理、另一个是Diff算法。

批处理:

在 React 中,并不是每一次 setState 都会触发一次更新,可能是多个 setState 最后才会合成并触发一次更新,这个对于性能的优化是非常关键的。官网中也有说明:

Diff算法:

当更新的逻辑被触发之后,会生成一颗新的 DOM 树,新旧两颗 DOM 树进行比较,得出差异。我们知道完全对比两颗树的性能是很差的,即便是最优的算法,时间复杂度也要达到 O(n^3),太慢了。React 中使用的 Diff 算法,并没有完全比较两颗树之间的差异,如果原来节点位置的 类型 变化时,会直接替换这颗子树,还有就是通过 key 来告诉 React 的元素位置变化,更多详细的规则可以查看官网的描述。

这套 Diff 算法,使时间复杂度从 O(n^3) 降低到了 O(n) ,整整降低了一个量级。

一些误解

Render 被调用,DOM 就更新了?

有些同学在看到 Render 函数被调用了,就以为这个组件被更新了,重新渲染了。其实不然,这个过程仅仅还只是在生成新的虚拟 DOM 树而已,还没到渲染 DOM 的步骤。

虚拟 DOM 牛皮,速度最快?

有些同学看到虚拟DOM 这套机制,就觉得很牛逼,甚至觉得比原生操作更快。借鉴了知乎上的回答:没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。

最后,逛了一圈知乎,似乎都在说现在的Web前端的趋势是 独立状态帧 + 多线程渲染 、函数式编程。其实 React 16 中引入的 函数式组件 + Hook,不就是这个趋势的实现吗?要学的东西又多了一些。。。。。。