虚拟DOM
为什么使用虚拟DOM
因为直接操作虚拟DOM效率低,并且执行慢
浏览器工作流
下图使用webkit,除了一些细微差别,所有浏览器工作流相同
创建DOM树:
- 一旦浏览器接收一个HTML文件,渲染引擎解析它并创建一个节点DOM树,HTML节点有一个一对一的关系
创建渲染树- 与此同时解析外部CSS文件,内联样式文件.样式信息随着DOM树中节点,被用作创建其他的树,被称为渲染树
创建渲染树的背后- 在webkit中,解析节点,样式的过程被称为“attachment”,DOM树中的所有节点都有一个attach方法,它接收计算出的样式信息并且返回一个渲染对象(renderer)
- Attachment 是同步的,节点插入DOM树会调用新节点的attach方法
- 构建渲染树过程中,由这些渲染对象组成的,需要计算每个渲染对象的视觉属性.这是通过元素的计算样式属性来完成的。
布局(重排)- 在构建渲染树之后,他经过一个布局过程,在屏幕上都会给渲染树中的每个节点一个确定的坐标,显示在屏幕的确切位置
绘制- 下一步就是绘制渲染对象-渲染树通过调用每一个节点的paint方法进行绘制(使用浏览器的后端API agnostic UI),最终展示内容在屏幕上
进入虚拟DOM
每当修改一个DOM行为时候,浏览器工作流中所有步骤都会触发,从创建渲染树开始(从新计算所有节点的的所有样式信息),布局,绘制,所有步骤都会从做
在一个复杂的过程中,通常涉及大量的DOM的操作,这需要多个计算步骤,这是效率非常低的
当你的视图变化时候,在真实DOM上进行所有假定的更改,首先生成虚拟DOM,然后发送到真实的DOM,从而减少计算的数量
实际上虚拟DOM是将双缓冲应用于虚拟DOM
DOM DIFF算法
什么是 DOM Diff 算法
Web 界面由 DOM 树来构成,当其中某一部分发生变化时,其实就是对应的某个 DOM 节点发生了变化.在虚拟dom中,状态决定界面,前后两种状态对应两种界面,比较两个界面的区别就需要比较两个界面
即给定任意两棵树,找到最少的转换步骤。但是标准的的 编辑距离算法复杂度需要 O(n^3),这显然无法满足性能要求。要达到每次界面都可以整体刷新界面的目的,势必需要对算法进行优化。FACEBOOK工程师做了以下两点如下假设,使算法复杂度降低到O(n):
- 两个相同组件产生类似的 DOM 结构,不同的组件产生不同的 DOM 结构;
- 对于同一层次的一组子节点,它们可以通过唯一的 id 进行区分。
不同节点类型的比较
为了在树之间进行比较,我们首先要能够比较两个节点,比较两个虚拟 DOM 节点,当两个节点不同时,应该如何处理。
这分为两种情况:
- (1)节点类型不同
- (2)节点类型相同,但是属性不同。
当在树中的同一位置前后输出了不同类型的节点,直接删除前面的节点,然后创建并插入新的节点。假设我们在树的同一位置前后两次输出不同类型的节点。
1
2
3 renderA: <div/>
renderB: <span/>
=> [removeNode <div/>], [insertNode <span/>]
当一个节点从 div 变成 span 时,简单的直接删除 div 节点,并插入一个新的 span 节点。这符合我们对真实 DOM 操作的理解。
需要注意的是,删除节点意味着彻底销毁该节点,而不是再后续的比较中再去看是否有另外一个节点等同于该删除的节点。如果该删除的节点之下有子节点,那么这些子节点也会被完全删除,它们也不会用于后面的比较。这也是算法复杂能够降低到 O(n)的原因。
不同的组件一般会产生不一样的 DOM 结构,与其浪费时间去比较它们基本上不会等价的 DOM 结构,还不如完全创建一个新的组件加上去。
DOM Diff 算法实际上只会对树进行逐层比较,如下所述。
逐层进行节点比较
树的算法其实非常简单,那就是两棵树只会对同一层次的节点进行比较。如下图所示:
只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
例,比如下面的DOM结构

A 节点被整个移动到 D 节点下,直观的考虑 DOM Diff 操作应该是
1 | A.parent.remove(A); |
只会简单的考虑同层节点的位置变换,对于不同层的节点,只有简单的创建和删除。当根节点发现子节点中 A 不见了,就会直接销毁 A;而当 D 发现自己多了一个子节点 A,则会创建一个新的 A 作为子节点。因此对于这种结构的转变的实际操作是
1
2
3
4
5 A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);
可以看到,以 A 为根节点的树被整个重新创建.虽然看上去这样的算法有些“简陋”,但是其基于的是第一个假设:两个不同组件一般产生不一样的 DOM 结构。
1
2
3
4
5
6
7 C will unmount.
C is created.
B is updated.
A is updated.
C did mount.
D is updated.
R is updated.
可以看到C节点是完全重建后再添加到 D 节点之下,而不是将其“移动”过去
相同类型节点的比较
相同类型的节点,对属性进行重设从而实现节点的转换。例如:
1
2
3 renderA: <div id="before" />
renderB: <div id="after" />
=> [replaceAttribute id "after"]
虚拟 DOM 的 style 属性稍有不同,其值并不是一个简单字符串而必须为一个对象,因此转换过程如下:
1
2
3 renderA: <div style={{color: 'red'}} />
renderB: <div style={{fontWeight: 'bold'}} />
=> [removeStyle color], [addStyle font-weight 'bold']
列表节点的比较
对于不在同一层的节点的比较,即使它们完全一样,也会销毁并重新创建。那么当它们在同一层时
列表节点的操作通常包括添加、删除和排序。例如下图,我们需要往 B 和 C 直接插入节点 F
这时如果每个节点都没有唯一的标识,React 无法识别每一个节点,那么更新过程会很低效,即,将 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入一个 E 节点。效果如下图所示:
会逐个对节点进行更新,转换到目标节点。而最后插入新的节点 E,涉及到的 DOM 操作非常多。而如果给每个节点唯一的标识(key),那么 React 能够找到正确的位置去插入新的节点,入下图所示:
对于列表节点顺序的调整其实也类似于插入或删除,
即将同一层的节点位置进行调整。如果未提供 key,那么认为 B 和 C 之后的对应位置组件类型不同,因此完全删除后重建,控制台输出如下:
1
2
3
4
5
6
7
8 B will unmount.
C will unmount.
C is created.
B is created.
C did mount.
B did mount.
A is updated.
R is updated.
而如果提供了 key,如下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 shape5: function() {
return (
<Root>
<A>
<B key="B" />
<C key="C" />
</A>
</Root>
);
},
shape6: function() {
return (
<Root>
<A>
<C key="C" />
<B key="B" />
</A>
</Root>
);
},
控制台输出如下:
1
2
3
4 C is updated.
B is updated.
A is updated.
R is updated.






