Virtual Dom

当前主流的前端框架 Vue 和 React 中都有着 Virtual Dom 的概念,相信大家都已经耳濡目染了。

在说 Virtual Dom 之前,我们线说说浏览器渲染页面的过程:

浏览器渲染

浏览器的渲染流程(简单版本):浏览器输入url,浏览器主进程接管,开一个下载线程,然后进行 http请求(略去DNS查询,IP寻址等等操作),然后等待响应,获取内容,随后将内容通过 RendererHost 接口转交给 Renderer 进程,然后就是浏览器渲染流程开始。

浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:

  1. 解析html建立dom树
  2. 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
  3. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  4. 绘制render树(paint),绘制页面像素信息
  5. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。

所有详细步骤都已经略去,渲染完毕后就是load事件了,之后就是自己的JS逻辑处理了。

但是,当render tree 中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。

当 render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。

Virtual Dom 上通过 diff 算法,当要更新 DOM (即回流与重绘)的时候,会通过 diff 寻找到要变更的 DOM 节点,再把这个修改更新到浏览器实际的 DOM 节点上,所以实际上不是真的渲染整个 DOM 树。这个虚拟 DOM 是一个纯粹的 JS 数据结构,所以性能会比原生 DOM 快很多。

什么是虚拟DOM

相比于频繁的手动去操作dom而带来性能问题,Virtual Dom 很好的将 dom 做了一层映射关系,进而将在我们本需要直接进行 dom 的一系列操作,映射到了操作 vdom,将原本需要在真实dom进行的创建节点,删除节点,添加节点等一系列复杂的 dom 操作全部放到 vdom 中进行,然后只将变化的地方更新到真实 DOM 上,这样就通过操作 vdom 来提高直接操作的 dom 的效率和性能。简单来说就是:

  • 用JS模拟DOM结构
  • 通过JS来对DOM变化对比
  • 减少DOM操作带来的性能损耗

比如我们定义了一个 vdom ,它的 js 数据结构是:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
tag: 'div'
data: {
id: 'app',
class: 'box'
},
children: [
{
tag: 'p',
text: 'this is demo'
}
]
}

最后渲染出的实际的dom结构就是:

1
2
3
<div id="app" class="box">
<p>this is demo</p>
</div>

React 基于 Virtual DOM 的数据更新与UI同步机制:

初始渲染时,首先将数据渲染为 Virtual DOM,然后由 Virtual DOM 生成 DOM:

数据更新时,渲染得到新的 Virtual DOM,与上一次得到的 Virtual DOM 进行 diff,得到所有需要在 DOM 上进行的变更,然后在 patch 过程中应用到 DOM 上实现UI的同步更新。

在 github 上有两个比较有名的 Virtual DOM 实现:

snabbdom的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script>
var snabbdom = window.snabbdom;
// 定义 patch
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义 h
var h = snabbdom.h;
var container = document.getElementById('container');
// 生成 vnode
var vnode = h('ul#list',{},[
h('li.item',{},'Item 1'),
h('li.item',{},'Item 2')
])
patch(container, vnode);
// 模拟数据改变
var btnChange = document.getElementById('btn-change');
btnChange.addEventListener('click',function(){
var newVnode = h('ul#list',{},[
h('li.item',{},'Item 1'),
h('li.item',{},'Item 222'),
h('li.item',{},'Item 333')
])
patch(vnode, newVnode);
})
</script>
</body>
</html>

如图,可以发现,只有修改了的数据才进行了刷新,减少了DOM操作,这其实就是 vnode与 newVnode 通过 diff 算法(深度优先)对比,找出改变了的地方,然后只重新渲染改变的。

参考文章: Change And Its Detection In JavaScript Frameworks



完~