初探 Virtual DOM

(一)什么是虚拟DOM

传统的 DOM 操作是直接在 DOM 上操作的,当需要修改一系列元素中的值时,就会直接对 DOM 进行操作。而采用 Virtual DOM 则会对需要修改的 DOM 进行 Diff 操作,只选择需要修改的部分。在React中,render执行的结果得到的并不是真正的DOM节点,而是轻量级的JavaScript对象,我们称之为Virtual DOM。
而对不需要大量操作 DOM 的页面来说,虚拟 DOM 可能不是最佳的,React 和 Vue 从来没保说过他们比原生操作 DOM 更快,引用腾讯 AlloyTeam 的一段话:

没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。
React给我们的保证是,在不需要手动优化的情况下,它依然可以给我们提供过得去的性能。

(二)为什么需要虚拟 DOM

大家都知道,DOM 操作是很慢的,其元素非常庞大,而JS操作则相对来说很“便宜”。
浏览器渲染引擎渲染 DOM 的开销那么大,怎么办呢,于是虚拟DOM 为我们提供的解决方案是把这些 DOM 操作都放在 JavaScript 引擎中,完全不会有这些开销。

(三)虚拟 DOM 实现原理

用 JS 对象模拟 DOM 树

用 JavaScript 来表示一个 DOM 节点是很简单的事情,你只需要记录它的节点类型、属性,还有子节点:
element.js

1
2
3
4
5
6
7
8
9
function Element (tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}

module.exports = function (tagName, props, children) {
return new Element(tagName, props, children)
}

例如上面的 DOM 结构就可以简单的表示

1
2
3
4
5
6
7
let el = require('./element)

let ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
])

ul现在只存在于 JavaScript对象中,页面中并不存在这个结构
render 方法会自动建立一个真正的 DOM 节点,设置这个节点的属性,然后递归地把自己的子节点也构建起来,最后把节点塞入文档中。

比较两棵虚拟 DOM 树的差异

比较两棵虚拟 DOM 树的差异是 Virtual DOM 算法最核心的部分,这也就是第一段提到的 Diff 操作。

  1. 不同层级下的节点操作
    首先 React 的 diff 策略有一条就是

    Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计

    但是如果真的出现了 DOM 节点跨层级的操作怎么办呢,React 的做法是 只有创建和删除,比如根目录下的A节点被移动到D节点下,那么 React 发现根节点下A消失了,就会直接删除A;在D节点下发现多出一个A节点,则会直接创建一个新的A节点。
    毫无疑问,这样是低效的,所以React官方建议不要进行DOM节点跨层级操作
    promise

  2. 同一层级下的节点操作

    当节点处于同一层级时,React diff 提供了三种节点操作,分别为:插入、移动和删除

    如下图,老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。
    promise

    React 发现这类操作繁琐冗余,因为这些都是相同的节点,但由于位置发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。

    针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分。

    新老集合所包含的节点,如下图所示,新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。
    promise

(四)结语

最后以 React Diff 的策略作为结语,Diff 作为虚拟 DOM 的核心,理解它对于理解虚拟 DOM 很有帮助.

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。

  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。

  3. 对于同一层级的一组子节点,它们可以通过唯一 key 进行区分。