什么是 Virtual DOM?

什么是 Virtual DOM?

最近在深入学习 Virtual DOM 的相关知识,参考了许多资料,也拜读了许多大神的文章,所以在这里大致的整理成了比较适合自己理解的方式,方便时不时回来翻翻,复习一下,篇幅较长,主要会分为三个部分来分别进行介绍,具体章节如下,目录名就差不多代表了章节的相关内容

本篇是第一部分,主要介绍 Virtual DOM 相关内容,主要参考的是 HcySunYang/vue-design,本章相关内容如下

  • 什么是 Virtual DOM
    • 如何将 Virtual DOM 渲染为真实的 DOM 节点
    • VNode 描述真实 DOM
    • VNode 的种类
  • 辅助创建 VNodeh 函数
    • 完善 h 函数
    • 使用 h 函数来创建 VNode

下面我们就一步一步来看

什么是 Virtual DOM?

在谈论 Virtual DOM 之前,我们必须要先理解什么是 DOMDOMDocument Object Model,是一种通过对象表示结构化文档的方式,DOM 是跨平台的,也是语言无关的(比如 HTMLXML 都可以用它表示与操作),浏览器处理 DOM 的实现细节,然后我们可以通过 JavaScriptCSS 来与它交互

DOM 的主要问题是没有为创建动态 UI 而优化,虽然可以使用 jQuery 这种可以用来简化 DOM 操作的类库,但是并没有解决大量 DOM 操作的性能问题,因为在大型页面或者单页应用里,动态的创建或销毁 DOM 的操作是很频繁的,DOM 操作是很慢的,比如新创建了一个 div,并不是只有单单一个 div 元素那么简单,这个元素上本身或者继承很多属性如 widthheightoffsetHeightstyletitle 等,另外还需要注册这个元素的诸多方法,比如 onfucosonclick 等等,这还只是一个元素,如果元素比较多的时候,还涉及到嵌套,那么元素的属性和方法等等就会很多,效率很低

比如我们来看下图,我们在一个空白网页的 body 中添加一个 div 元素(为了偷懒就直接把百度的首页掏空添加了一个空的 div

这个元素会挂载默认的 styles,得到这个元素的 computed 属性,注册相应的 Event ListenerDOM Breakpoints 以及大量的 properties,这些属性和方法的注册肯定是需要耗费大量时间的(看右侧的滚动条就知道需要挂载多少内容了)

Virtual DOM 就是解决问题的一种探索,Virtual DOM 建立在 DOM 之上,是基于 DOM 的一层抽象,实际可理解为用更轻量的纯 JavaScript 对象(树)来描述 DOM(树),操作 JavaScript 对象当然比操作 DOM 快,因为不用更新屏幕,我们可以随意改变 Virtual DOM,然后仅仅将需要改变的地方再更新到 DOM

如何将 Virtual DOM 渲染为真实的 DOM 节点

从这一部分开始,我们为了简便,将会使用 VNode 来简称 Virtual DOM(其实只需要知道它们是同一个东西即可)

VNode 是真实 DOM 的描述,比如我们可以用如下对象描述一个 div 标签

1
2
3
const elementVnode = {
tag: 'div'
}

想要把 elementVnode 渲染成真实 DOM,我们还需要一个渲染器(Renderer),下面是一个简单的实现

1
2
3
4
5
6
7
8
9
10
11
function render(vnode, container) {
mountElement(vnode, container)
}

// 这样是有一定缺陷的,我们后面将会来完善它
function mountElement(vnode, container) {
// 创建元素
const el = document.createElement(vnode.tag)
// 将元素添加到容器
container.appendChild(el)
}

为了渲染之前的 div 标签,我们可以这样调用 render 函数

1
2
// 把 elementVnode 渲染到 id 为 app 的元素下
render(elementVnode, document.getElementById('app'))

上述只是一个简单的示例,只能适用于普通的标签,但并不适用于『组件』,为了能够渲染组件,我们还需要思考组件的 VNode 应该如何表示?对于 HTML 标签的 VNode 来说,其 tag 属性的值就是标签的名字,但如果是组件的话,我们可以将其 VNode 中的 tag 属性指向组件自身,比如如下组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个 render 方法,里面返回指定的 tag
class MyComponent {
// render 函数产出 VNode
render() {
return {
tag: 'div'
}
}
}

// 这样来描述
const componentVnode = {
tag: MyComponent
}

但想要正确地渲染该组件,我们还需要修改我们的 render 函数,我们可以通过判断 vnode.tag 是否是字符串 来区分一个 VNode 到底是 HTML 标签还是组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function render(vnode, container) {
if (typeof vnode.tag === 'string') {
// html 标签
mountElement(vnode, container)
} else {
// 组件
mountComponent(vnode, container)
}
}

// 挂载组件方法
function mountComponent(vnode, container) {
// 创建组件实例
const instance = new vnode.tag()
// 渲染
instance.$vnode = instance.render()
// 挂载
mountElement(instance.$vnode, container)
}

是不是已经有了一点思路了,我们就可以按照以上的方式逐渐的丰富我们的 VNoderender 方法,使其的通用性更强

用 VNode 描述真实 DOM

那么一个 VNode 到底需要拥有哪些属性呢,我们一点一点来讨论,如下

  • 首先我们使用 tag 属性来存储标签的名字
  • 可以用 data 属性来存储该标签的附加信息,比如 styleclassevent 等,通常我们把一个 VNode 对象的 data 属性称为 VNodeData
  • 为了描述子节点,我们需要给 VNode 对象添加 children 属性,若有多个子节点,则可以把 children 属性设计为一个数组
  • 除了标签元素之外,DOM 中还有文本节点,由于文本节点没有标签名字,所以它的 tag 属性值为 null,由于文本节点也无需用额外的 VNodeData 来描述附加属性,所以其 data 属性值也是 null

针对以上内容,我们可以简单的整理出两种类型的描述,『普通标签』和『文本节点』

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
// 一个 div 标签,具有 style 属性和两个子节点
const elementVNode = {
tag: 'div',
data: {
style: {
width: '100px',
height: '100px'
}
},
children: [
{
tag: 'h1',
data: null
},
{
tag: 'p',
data: null
}
]
}

// 一个以文本节点作为子节点的 div 标签的 VNode 对象
const elementVNode = {
tag: 'div',
data: null,
children: {
tag: null,
data: null,
children: '文本内容'
}
}

下面我们再来看看如何描述组件,我们之前提到过,可以通过检查 tag 属性值是否是字符串来确定一个 VNode 是否是普通标签,即

1
2
3
<div>
<MyComponent />
</div>

对应的 VNode

1
2
3
4
5
6
7
8
const elementVNode = {
tag: 'div',
data: null,
children: {
tag: MyComponent,
data: null
}
}

另外还有两种特殊的组件,即 FragmentPortal,具体的使用场景就不详细展开了,我们只来看看如何进行表示,我们使用 tag 来标记 Fragment,当渲染器在渲染 VNode 时,如果发现该 VNode 的类型是 Fragment,就只需要把该 VNode 的子节点渲染到页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Fragment = Symbol()
const fragmentVNode = {
// tag 属性值是一个唯一标识
tag: Fragment,
data: null,
children: [
{
tag: 'td',
data: null
},
{
tag: 'td',
data: null
},
{
tag: 'td',
data: null
}
]
}

同样的针对 Portal,我们可以得出以下的对应关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<Portal target="#app-root">
<div class="overlay"></div>
</Portal>
</template>

// 对应着

const Portal = Symbol()
const portalVNode = {
tag: Portal,
data: {
target: '#app-root'
},
children: {
tag: 'div',
data: {
class: 'overlay'
}
}
}

Portal 类型的 VNodeFragment 类型的 VNode 类似,都需要一个唯一的标识,来区分其类型,目的是告诉渲染器如何渲染该 VNode

VNode 的种类

不同类型的 VNode 拥有不同的设计,我们可以把 VNode 分成五类,分别是 html/svg 元素、组件、纯文本、Fragment 以及 Portal,如下图所示

但是这里会存在一个问题,比如之前我们在判断需要挂载的对象是标签还是组件的时候,使用的是通过检查 tag 属性值是否是字符串来确定一个 VNode 是否是普通标签,这样是不严谨的,所以我们就有必要使用一个唯一的标识,来标明某一个 VNode 具体是属于哪一类,我们只需要为每一个 VNode 种类都分配一个 flags 值即可,在 JavaScript 里就用一个对象来表示即可

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
// flags.js
const VNodeFlags = {

ELEMENT_HTML: 1, // html 标签
ELEMENT_SVG: 1 << 1, // SVG 标签

COMPONENT_STATEFUL_NORMAL: 1 << 2, // 普通有状态组件
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE: 1 << 3, // 需要被 keepAlive 的有状态组件
COMPONENT_STATEFUL_KEPT_ALIVE: 1 << 4, // 已经被 keepAlive 的有状态组件
COMPONENT_FUNCTIONAL: 1 << 5, // 函数式组件

TEXT: 1 << 6, // 纯文本
FRAGMENT: 1 << 7, // Fragment
PORTAL: 1 << 8 // Portal
}

// 上述枚举属性的值基本都是通过将十进制数字 1 左移不同的位数得来的
// 根据这些基本的枚举属性值,我们还可以派生出额外的三个标识

// html 和 svg 都是标签元素,可以用 ELEMENT 表示
VNodeFlags.ELEMENT = VNodeFlags.ELEMENT_HTML | VNodeFlags.ELEMENT_SVG

// 普通有状态组件、需要被 keepAlive 的有状态组件、已经被 keepAlice 的有状态组件都是有状态组件,统一用 COMPONENT_STATEFUL 表示
VNodeFlags.COMPONENT_STATEFUL =
VNodeFlags.COMPONENT_STATEFUL_NORMAL |
VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |
VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE

// ==========================================================================================
// ==========================================================================================

// 关于 children 和 ChildrenFlags
// 总的来说无非有以下几种
// 1. 没有子节点
// 2. 只有一个子节点
// 3. 多个子节点(分为有 key 和无 key 的情况)
// 4. 不知道子节点的情况
// 至于为什么 children 也需要标识是为了后续在 diff 当中来进行优化

// 有状态组件 和 函数式组件都是 组件,用 COMPONENT 表示
VNodeFlags.COMPONENT =
VNodeFlags.COMPONENT_STATEFUL | VNodeFlags.COMPONENT_FUNCTIONAL

const ChildrenFlags = {

UNKNOWN_CHILDREN: 0, // 未知的 children 类型
NO_CHILDREN: 1, // 没有 children
SINGLE_VNODE: 1 << 1, // children 是单个 VNode

KEYED_VNODES: 1 << 2, // children 是多个拥有 key 的 VNode
NONE_KEYED_VNODES: 1 << 3 // children 是多个没有 key 的 VNode
}

ChildrenFlags.MULTIPLE_VNODES =
ChildrenFlags.KEYED_VNODES | ChildrenFlags.NONE_KEYED_VNODES

export { VNodeFlags, ChildrenFlags }

这里简单的介绍一下位运算,如下是利用 VNodeFlags 判断 VNode 类型的例子,比如判断一个 VNode 是否是组件

1
2
3
4
// 使用按位与(&)运算
functionalComponentVnode.flags & VNodeFlags.COMPONENT // true
normalComponentVnode.flags & VNodeFlags.COMPONENT // true
htmlVnode.flags & VNodeFlags.COMPONENT // false

来看下表

VNodeFlags 左移运算 32 位的 bit 序列(出于简略,只用 9 位表示)
ELEMENT_HTML 00000000 1
ELEMENT_SVG 1 << 1 0000000 1 0
COMPONENT_STATEFUL_NORMAL 1 << 2 000000 1 00
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE 1 << 3 00000 1 000
COMPONENT_STATEFUL_KEPT_ALIVE 1 << 4 0000 1 0000
COMPONENT_FUNCTIONAL 1 << 5 000 1 00000
TEXT 1 << 6 00 1 000000
FRAGMENT 1 << 7 0 1 0000000
PORTAL 1 << 8 1 00000000

根据上表展示的基本 flags 值可以很容易地得出下表

VNodeFlags 32 位的 bit 序列(出于简略,只用 9 位表示)
ELEMENT 00000001 1
COMPONENT_STATEFUL 00001 1 100
COMPONENT 000 1 1 1 1 00

所以很自然的,只有 VNodeFlags.ELEMENT_HTMLVNodeFlags.ELEMENT_SVGVNodeFlags.ELEMENT 进行按位与(&)运算才会得到非零值,即为真

更多关于此处的内容可以参考 MDN-标志位与掩码


有了这些 flags 之后,我们在创建 VNode 的时候就可以预先为其打上 flags,以标明该 VNode 的类型

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// html 元素节点
const htmlVnode = {
flags: VNodeFlags.ELEMENT_HTML,
tag: 'div',
data: null
}

// svg 元素节点
const svgVnode = {
flags: VNodeFlags.ELEMENT_SVG,
tag: 'svg',
data: null
}

// 函数式组件
const functionalComponentVnode = {
flags: VNodeFlags.COMPONENT_FUNCTIONAL,
tag: MyFunctionalComponent
}

// 普通的有状态组件
const normalComponentVnode = {
flags: VNodeFlags.COMPONENT_STATEFUL_NORMAL,
tag: MyStatefulComponent
}

// Fragment
const fragmentVnode = {
flags: VNodeFlags.FRAGMENT,
// 注意,由于 flags 的存在,我们已经不需要使用 tag 属性来存储唯一标识
tag: null
}

// Portal
const portalVnode = {
flags: VNodeFlags.PORTAL,
// 注意,由于 flags 的存在,我们已经不需要使用 tag 属性来存储唯一标识,tag 属性用来存储 Portal 的 target
tag: target
}

// ========================================================================
// ========================================================================

// 没有子节点的 div 标签
const elementVNode = {
flags: VNodeFlags.ELEMENT_HTML,
tag: 'div',
data: null,
children: null,
childFlags: ChildrenFlags.NO_CHILDREN
}

// 文本节点的 childFlags 始终都是 NO_CHILDREN
const textVNode = {
tag: null,
data: null,
children: '我是文本',
childFlags: ChildrenFlags.NO_CHILDREN
}

// 拥有多个使用了key的 li 标签作为子节点的 ul 标签
const elementVNode = {
flags: VNodeFlags.ELEMENT_HTML,
tag: 'ul',
data: null,
childFlags: ChildrenFlags.KEYED_VNODES,
children: [
{
tag: 'li',
data: null,
key: 0
},
{
tag: 'li',
data: null,
key: 1
}
]
}

// 只有一个子节点的 Fragment
const elementVNode = {
flags: VNodeFlags.FRAGMENT,
tag: null,
data: null,
childFlags: ChildrenFlags.SINGLE_VNODE,
children: {
tag: 'p',
data: null
}
}

那么最后就只剩下 VNodeVNodeData 属性,它其实也是一个对象,不过我们会留在后面进行介绍,至此,我们已经对 VNode 完成了一定的设计,目前为止我们所设计的 VNode 对象如下

1
2
3
4
5
6
7
8
9
10
11
export interface VNode {
// _isVNode 属性在上文中没有提到,它是一个始终为 true 的值,有了它,我们就可以判断一个对象是否是 VNode 对象
_isVNode: true
// el 属性在上文中也没有提到,当一个 VNode 被渲染为真实 DOM 之后,el 属性的值会引用该真实DOM
el: Element | null
flags: VNodeFlags
tag: string | FunctionalComponent | ComponentClass | null
data: VNodeData | null
children: VNodeChildren
childFlags: ChildrenFlags
}

辅助创建 VNode 的 h 函数

我们之前已经介绍了 VNode 的种类和一些其他相关概念,但是在实际开发过程当中,去手写 VNode 肯定是不太现实的,所以我们需要一个可以帮助我们创建 VNode 对象的函数,在这里我们将其命名为 h,先来看一个最简单的 h 函数

1
2
3
4
5
6
7
8
9
10
11
function h() {
return {
_isVNode: true,
flags: VNodeFlags.ELEMENT_HTML,
tag: 'h1',
data: null,
children: null,
childFlags: ChildrenFlags.NO_CHILDREN,
el: null
}
}

这个 h 函数只能生成用来描述一个空的 <h1></h1>,实际上并没有太大的意义,接下来我们会来一一完善它

完善 h 函数

首先需要确定参数,实际上只需要把 tagdatachildren 提取为参数即可

1
2
3
4
5
6
7
8
// 为什么三个参数就能满足需求,对于 _isVNode 属性,它的值始终都为 true,所以不需要提取到参数中
// 对于 flags 属性,我们可以通过检查 tag 属性值的特征来确定该 VNode 的 flags 属性
function h(tag, data = null, children = null) {
let flags = null
if (typeof tag === 'string') {
flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
}
}

下面我们就来针对各种类型来单独处理,详细内容可以参考注释

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { VNodeFlags, ChildrenFlags } from './flags'

export const Fragment = Symbol()
export const Portal = Symbol()

export function h(tag, data = null, children = null) {
let flags = null

// 如果 tag 是字符串则可以确定该 VNode 是标签元素
// 再次通过 tag === 'svg' 进一步判断是否是 SVG 标签,从而确定了该 VNode 的类型
if (typeof tag === 'string') {
flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML

// 序列化 class(这个将在后面介绍挂载 class 属性的时候来进行介绍)
if (data) {
data.class = normalizeClass(data.class)
}

// 对于 Fragment 类型的 VNode,它的 tag 属性值为 null,但是纯文本类型的 VNode 其 tag 属性值也是 null
// 所以为了区分,我们可以增加一个唯一的标识,当 h 函数的第一个参数(tag)的值等于该标识的时候,则意味着创建的是 Fragment 类型的 VNode
} else if (tag === Fragment) {
flags = VNodeFlags.FRAGMENT

// 对于Portal 类型的 VNode,它的 tag 属性值也可以是字符串,这就会与普通标签元素类型的 VNode 冲突
// 所以同上,增加一个 Portal 标识
} else if (tag === Portal) {
flags = VNodeFlags.PORTAL
// 这里需要注意,其 tag 属性值存储的是 Portal 挂载的目标,即 target
// 通常模板在经过编译后,我们把 target 数据存储在 VNodeData 中
tag = data && data.target

// 如果一个 VNode 对象的 tag 属性值不满足以上全部条件,那只有一种可能了,即该 VNode 是组件
// 当然也有可能是文本节点,但是一般不会使用 h 去创建文本节点
// 一般在检测到该节点是文本节点的时候会为其自动创建一个纯文本的 VNode 对象
} else {
// 兼容 Vue2 的对象式组件
// 如果是 Vue2 的对象式组件,我们通过检查该对象的 functional 属性的真假来判断该组件是否是函数式组件
if (tag !== null && typeof tag === 'object') {
flags = tag.functional
? VNodeFlags.COMPONENT_FUNCTIONAL // 函数式组件
: VNodeFlags.COMPONENT_STATEFUL_NORMAL // 有状态组件

// Vue3 的类组件
// 在 Vue3 中,因为有状态组件会继承基类,所以通过原型链判断其原型中是否有 render 函数的定义来确定该组件是否是有状态组件
// 因为都是使用的 extends 来继承基类的,而子类通常都会有一个 render 方法
} else if (typeof tag === 'function') {
flags =
tag.prototype && tag.prototype.render
? VNodeFlags.COMPONENT_STATEFUL_NORMAL // 有状态组件
: VNodeFlags.COMPONENT_FUNCTIONAL // 函数式组件
}
}

// 同样的,可以使用上面类似的方法来确定 childFlags
// 1. children 是一个数组 ==> h('ul', null, [ h('li'), h('li') ])
// 2. children 是一个 VNode 对象 ==> h('div', null, h('span'))
// 3. 无 children ==> h('div')
// 4. children 是一个普通文本字符串 ==> h('div', null, '我是文本')
let childFlags = null
if (Array.isArray(children)) {
const { length } = children

// 没有 children
if (length === 0) {
childFlags = ChildrenFlags.NO_CHILDREN

// 单个子节点
} else if (length === 1) {
childFlags = ChildrenFlags.SINGLE_VNODE
children = children[0]

// 多个子节点,且子节点使用 key
// 这里有个问题,为什么多个子节点时会直接被当做使用了 key 的子节点
// 这个可以参考 normalizeVNodes() 这个函数,如果没有,我们手动进行了添加
} else {
childFlags = ChildrenFlags.KEYED_VNODES
children = normalizeVNodes(children)
}

// 如果 children 不是数组,并且没有子节点
} else if (children == null) {
childFlags = ChildrenFlags.NO_CHILDREN

// 如果 children 不是数组,而且是单个子节点
} else if (children._isVNode) {
childFlags = ChildrenFlags.SINGLE_VNODE

// 如果 children 不满足以上任何条件,则会把 children 作为纯文本节点的文本内容处理
// 即单个子节点,会调用 createTextVNode 创建纯文本类型的 VNode
} else {
childFlags = ChildrenFlags.SINGLE_VNODE
children = createTextVNode(children + '')
}

/**
*
* 这里有个需要注意的地方
* 以上用于确定 childFlags 的代码仅限于非组件类型的 VNode,因为对于组件类型的 VNode 来说,它并没有子节点
* 所有子节点都应该作为 slots 存在,所以如果使用 h 函数创建一个组件类型的 VNode
* 那么我们应该把 children 的内容转化为 slots,然后再把 children 置为 null
* 后续会进行介绍
*
*/

// 返回 VNode 对象
return {
_isVNode: true,
flags,
tag,
data,
// 如果 VNodeData 中存在 key 属性,则我们会把其添加到 VNode 对象本身
// 这个属性将在后面用来在 diff 算法当中保持映射关系
key: data && data.key ? data.key : null,
children,
childFlags,
el: null
}
}

// 序列化 class(针对 class 是字符串,数组或对象单独进行处理)
function normalizeClass(classValue) {
let res = ''
if (typeof classValue === 'string') {
res = classValue
} else if (Array.isArray(classValue)) {
for (let i = 0; i < classValue.length; i++) {
res += normalizeClass(classValue[i]) + ' '
}
} else if (typeof classValue === 'object') {
for (const name in classValue) {
if (classValue[name]) {
res += name + ' '
}
}
}
return res.trim()
}

// 手动添加 key
function normalizeVNodes(children) {
const newChildren = []
// 遍历 children
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.key == null) {
// 如果原来的 VNode 没有 key,则使用竖线(|)与该 VNode 在数组中的索引拼接而成的字符串作为 key
child.key = '|' + i
}
newChildren.push(child)
}
// 返回新的children,此时 children 的类型就是 ChildrenFlags.KEYED_VNODES
return newChildren
}

// 创建文本节点
function createTextVNode(text) {
return {
_isVNode: true,
// flags 是 VNodeFlags.TEXT
flags: VNodeFlags.TEXT,
tag: null,
data: null,
// 纯文本类型的 VNode,其 children 属性存储的是与之相符的文本内容
children: text,
// 文本节点没有子节点
childFlags: ChildrenFlags.NO_CHILDREN
}
}

使用 h 函数来创建 VNode

最后我们来看一些使用 h 函数的实际效果

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
// 模版
<template>
<div>
<span></span>
</div>
</template>

// 使用
const elementVNode = h('div', null, h('span'))

// 生成的 VNode 对象
const elementVNode = {
_isVNode: true,
flags: 1, // VNodeFlags.ELEMENT_HTML
tag: 'div',
data: null,
children: {
_isVNode: true,
flags: 1, // VNodeFlags.ELEMENT_HTML
tag: 'span',
data: null,
children: null,
childFlags: 1, // ChildrenFlags.NO_CHILDREN
el: null
},
childFlags: 2, // ChildrenFlags.SINGLE_VNODE
el: null
}

// ===============================================================
// ===============================================================

// 模版
<template>
<div>我是文本</div>
</template>

// 使用
const elementWithTextVNode = h('div', null, '我是文本')

// 生成的 VNode 对象
const elementWithTextVNode = {
_isVNode: true,
flags: 1, // VNodeFlags.ELEMENT_HTML
tag: 'div',
data: null,
children: {
_isVNode: true,
flags: 64, // VNodeFlags.TEXT
tag: null,
data: null,
children: '我是文本',
childFlags: 1, // ChildrenFlags.NO_CHILDREN
el: null
},
childFlags: 2, // ChildrenFlags.SINGLE_VNODE
el: null
}

// ===============================================================
// ===============================================================

// 模版
<template>
<td></td>
<td></td>
</template>

// 使用
const fragmentVNode = h(Fragment, null, [ h('td'), h('td') ])

// 生成的 VNode 对象
const fragmentVNode = {
_isVNode: true,
flags: 128, // VNodeFlags.FRAGMENT
data: null,
children: [
{
_isVNode: true,
flags: 1, // VNodeFlags.ELEMENT_HTML
tag: 'td',
data: null,
children: null,
childFlags: 1, // ChildrenFlags.NO_CHILDREN
key: '|0', // 自动生成的 key(可以发现,children 数组中的每一个 VNode 都自动添加了 key 属性)
el: null
},
{
_isVNode: true,
flags: 1, // VNodeFlags.ELEMENT_HTML
tag: 'td',
data: null,
children: null,
childFlags: 1, // ChildrenFlags.NO_CHILDREN
key: '|1', // 自动生成的 key
el: null
}
],
childFlags: 4, // ChildrenFlags.KEYED_VNODES
el: null
}

// ===============================================================
// ===============================================================

// 模版
<template>
<Portal target="#box">
<h1></h1>
</Portal>
</template>

// 使用
const portalVNode = h(Portal, { target: '#box' }, h('h1'))

// 生成的 VNode 对象
const portalVNode = {
_isVNode: true,
flags: 256, // VNodeFlags.PORTAL
tag: '#box', // 类型为 Portal 的 VNode,其 tag 属性值等于 data.target
data: { target: '#box' },
children: {
_isVNode: true,
flags: 1, // VNodeFlags.ELEMENT_HTML
tag: 'h1',
data: null,
children: null,
childFlags: 1, // ChildrenFlags.NO_CHILDREN
el: null
},
childFlags: 2, // ChildrenFlags.SINGLE_VNODE
el: null
}

// ===============================================================
// ===============================================================

// 模版(该模板中包含了一个函数式组件,并为该组件提供了一个空的 div 标签作为默认的插槽内容)
<template>
<MyFunctionalComponent>
<div></div>
</MyFunctionalComponent>
</template>

// 使用(一个函数式组件)
function MyFunctionalComponent() {}

// 传递给 h 函数的第一个参数就是组件函数本身
const functionalComponentVNode = h(MyFunctionalComponent, null, h('div'))

// 生成的 VNode 对象
// 暂且这样设计,等到后续涉及到插槽内容的时候再来细说
// 为什么我们不使用 children 属性来存储插槽内容,以及我们应该如何使用 VNode 来描述插槽
const functionalComponentVNode = {
_isVNode: true,
flags: 32, // VNodeFlags.COMPONENT_FUNCTIONAL
tag: MyFunctionalComponent, // tag 属性值引用组件函数
data: null,
children: {
_isVNode: true,
flags: 1,
tag: 'div',
data: null,
children: null,
childFlags: 1,
el: null
},
childFlags: 2, // ChildrenFlags.SINGLE_VNODE
el: null
}

// ===============================================================
// ===============================================================

// 模版(有状态组件应该继承 Component)
class MyStatefulComponent extends Component {}

// 使用
const statefulComponentVNode = h(MyStatefulComponent, null, h('div'))

// 生成的 VNode 对象
const statefulComponentVNode = {
_isVNode: true,
// VNodeFlags.COMPONENT_STATEFUL_NORMAL
// 这里需要注意,只有当组件的原型上拥有 render 函数时才会把它当作有状态组件
flags: 4,
data: null,
children: {
_isVNode: true,
flags: 1,
tag: 'div',
data: null,
children: null,
childFlags: 1,
el: null
},
childFlags: 2,
el: null
}

现在,我们的 h 函数已经可以创建任何类型的 VNode 对象了,有了 VNode 对象,我们下一步要做的就是将 VNode 对象渲染成真实 DOM

参考

如果想了解更多的相关内容,可以参考以下链接

# React

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×