Vue.js的DOM-Diff

前言

Virtual DOM 这个概念相信大部分人都不会陌生,它产生的前提是浏览器中的 DOM 是很“昂贵”的,为了更直观的感受,我们可以简单的把一个简单的 div 元素的属性都打印出来,如图所示:

(dom)

上图中我们打印一个简单的空 div 标签,就打印出这么多东西,更不用说复杂的、深嵌套的 DOM 节点了。由此可见,直接操作真实 DOM 是非常消耗性能的。
而 Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。在 Vue.js 中,Virtual DOM 是用 VNode 这么一个 Class 去描述,它是定义在 src/core/vdom/vnode.js 中的。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node

// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support

constructor(
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag; /*当前节点的标签名*/
this.data =
data; /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.children = children; /*当前节点的子节点,是一个数组*/
this.text = text; /*当前节点的文本*/
this.elm = elm; /*当前虚拟节点对应的真实dom节点*/
this.ns = undefined; /*当前节点的名字空间*/
this.context = context; /*当前组件节点对应的Vue实例*/
this.fnContext = undefined; /*函数式组件对应的Vue实例*/
this.fnOptions = undefined;
this.fnScopeId = undefined;
this.key = data && data.key; /*节点的key属性,被当作节点的标志,用以优化*/
this.componentOptions = componentOptions; /*组件的option选项*/
this.componentInstance = undefined; /*当前节点对应的组件的实例*/
this.parent = undefined; /*当前节点的父节点*/
this.raw = false; /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.isStatic = false; /*静态节点标志*/
this.isRootInsert = true; /*是否作为根节点插入*/
this.isComment = false; /*是否为注释节点*/
this.isCloned = false; /*是否为克隆节点*/
this.isOnce = false; /*是否有v-once指令*/
this.asyncFactory = asyncFactory;
this.asyncMeta = undefined;
this.isAsyncPlaceholder = false;
}

// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child(): Component | void {
return this.componentInstance;
}
}

可以看到 Vue.js 中的 Virtual DOM 的定义还是略微复杂一些的,因为它这里包含了很多 Vue.js 的特性。这里千万不要被这些茫茫多的属性吓到,实际上 Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom 的实现,然后加入了一些 Vue.js 特色的东西。我建议大家如果想深入了解 Vue.js 的 Virtual DOM 前不妨先阅读这个库的源码,因为它更加简单和纯粹。

VNode 的类型

VNode 类可以通过不同属性的搭配来描述出各种类型的真实 DOM 节点。那么它都可以描述出哪些类型的节点呢?通过阅读源码,发现可以描述出以下几种类型的节点。

  • 注释节点
  • 文本节点
  • 元素节点
  • 组件节点
  • 函数式组件节点
  • 克隆节点

接下来,我们就把这几种类型的节点描述方式从源码中一一对应起来。

注释节点

注释节点描述起来相对就非常简单了,它只需两个属性就够了,源码如下:

1
2
3
4
5
6
export const createEmptyVNode = (text: string = "") => {
const node = new VNode();
node.text = text;
node.isComment = true;
return node;
};

从上面代码中可以看到,描述一个注释节点只需两个属性,分别是:text 和 isComment。其中 text 属性表示具体的注释信息,isComment 是一个标志,用来标识一个节点是否是注释节点。

文本节点

文本节点描述起来比注释节点更简单,因为它只需要一个属性,那就是 text 属性,用来表示具体的文本信息。源码如下:

1
2
3
export function createTextVNode(val: string | number) {
return new VNode(undefined, undefined, undefined, String(val));
}

克隆节点

克隆节点就是把一个已经存在的节点复制一份出来,它主要是为了做模板编译优化时使用,这个后面我们会说到。关于克隆节点的描述,源码如下:

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
export function cloneVNode(vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
);
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
cloned.isComment = vnode.isComment;
cloned.fnContext = vnode.fnContext;
cloned.fnOptions = vnode.fnOptions;
cloned.fnScopeId = vnode.fnScopeId;
cloned.asyncMeta = vnode.asyncMeta;
cloned.isCloned = true;
return cloned;
}

从上面代码中可以看到,克隆节点就是把已有节点的属性全部复制到新节点中,而现有节点和新克隆得到的节点之间唯一的不同就是克隆得到的节点 isCloned 为 true。

元素节点

相比之下,元素节点更贴近于我们通常看到的真实 DOM 节点,它有描述节点标签名词的 tag 属性,描述节点属性如 class、attributes 等的 data 属性,有描述包含的子节点信息的 children 属性等。由于元素节点所包含的情况相比而言比较复杂,源码中没有像前三种节点一样直接写死(当然也不可能写死),那就举个简单例子说明一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 真实DOM节点
<div id='a'><span>难凉热血</span></div>

// VNode节点
{
tag:'div',
data:{},
children:[
{
tag:'span',
text:'难凉热血'
}
]
}

我们可以看到,真实 DOM 节点中:div 标签里面包含了一个 span 标签,而 span 标签里面有一段文本。反应到 VNode 节点上就如上所示:tag 表示标签名,data 表示标签的属性 id 等,children 表示子节点数组。

组件节点

组件节点除了有元素节点具有的属性之外,它还有两个特有的属性:

  • componentOptions :组件的 option 选项,如组件的 props 等
  • componentInstance :当前组件节点对应的 Vue 实例

函数式组件节点

函数式组件节点相较于组件节点,它又有两个特有的属性:

  • fnContext:函数式组件对应的 Vue 实例
  • fnOptions: 组件的 option 选项

总结

其实 VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。

Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程。有了数据变化前后的VNode,我们才能进行后续的DOM-Diff找出差异,最终做到只更新有差异的视图,从而达到尽可能少的操作真实DOM的目的,以节省性能。

参考文献