# 虚拟DOM
# 什么是虚拟DOM
虚拟DOM(元素)是一个一般的js对象,准确的说是一个倒立的对象树
虚拟DOM保存了真实DOM的层次关系和基本属性,与真实DOM一一对应
如果只是更新虚拟DOM,页面不会重绘,大大提高了绘制效率。
# 为什么虚拟DOM更快
- 1、减少DOM操作的次数
虚拟DOM可以将多次操作合并成一次操作;
比如,添加1000个节点,传统方式是一个个的添加,使用虚拟DOM 是一次性添加。
- 2、减少DOM操作的范围
虚拟DOM可以借助DOM diff 把多余的操作省掉;
借助 DOM diff,检测时新增、删除和更改的DOM,仅仅对这些进行更改。
比如,添加1000个节点基础上,再添加10个,传统方式是循环一个个的添加,操作1010次,使用虚拟 DOM 是,对比发现仅仅有10元素是新增的,然后,一次性添加10节点。
# 虚拟DOM长什么样
const cNode = {
key: null,
type: 'div',
props: {
children: [
{type: 'span', ...},
{type: 'span', ...}
],
className: 'red',
onClick: () => {}
},
ref: null,
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 虚拟DOM创建
createElement
方法可以帮我们创建一个虚拟DOM,来模仿你想要的真实DOM的结构
参数:
- 1、tag:DOM元素的标签( 'a' , 'p ', 'div' ...)
- 2、props:DOM元素的属性( {className:'box' , id:'container' , style:{fontSize:'20px'} , key:1 , ...})
- 3、children:DOM元素的内容( 字符串或其他虚拟DOM元素组成的数组 )
// 用 createElement 方法创建一个虚拟DOM的结构
let virtualDOM = createElement('div',{id:'container'},[
createElement('p',{className:'msg',key:1},'这是一条消息'),
createElement('p',{className:'msg',key:2},'这是另一条消息'),
createElement(null,null,'这是一条没有标签包裹的文本'),
createElement('button',{className:'btn',key:3},'按钮')
]);
1
2
3
4
5
6
7
2
3
4
5
6
7
但是,在我们实际的开发中是这样的
<div id="container">
<p className="msg" key={1}>这是一条消息</p>
<p className="msg" key={2}>这是另一条消息</p>
这是一条没有标签包裹的文本
<button className="btn" key={2}>按钮</button>
</div>
1
2
3
4
5
6
2
3
4
5
6
实际上是我们通过 loader babel 将 我们的jsx语法转化成 createElement 的形式。
# 虚拟DOM到真实的DOM
render方法:render( virtualDOM, DOM )
render
方法可以将你的虚拟DOM解析成真实的DOM并渲染到页面上
参数:
- 1、virtualDOM:需要解析的虚拟DOM
- 2、DOM:需要渲染在哪个DOM里
render(virtualDOM,document.getElementById('root'));
1
# 虚拟DOM原理
# 1、初始化:定义类型
// 定义一个tag类型集合
const tagTypes = {
HTML:"HTML",
TEXT:"TEXT"
}
// 定义children类型集合
const childrenTypes = {
// 子元素只有一个 说明是字符串
single:"single",
// 子元素是一个数组 数组里是多个元素
many:"many",
// 子元素是一个空 没有子元素
empty:"empty"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2、createElement()
实现原理:根据你传入参数的类型,返回一个对应出来你想要的结构的整合后的JS对象。
// 创建虚拟dom的方法
function createElment(tag,props,children){
// 定义tag类型
let type;
// 如果tag存在,那么该元素就是HTML元素,否则是字符串
if(typeof tag === 'string'){
type = tagTypes.HTML;
}else{
type = tagTypes.TEXT;
}
// 定义children类型
let childrenType;
// 如果children是文本的时候就创建一个文本虚拟dom
// 如果children是数组的时候就创建一个有子节点的虚拟dom
// 如果children是空的时候就创建一个空虚拟dom
if(typeof children === 'string'){
childrenType = childrenTypes.single;
// createTextNode:创建文本DOM方法
children = createTextNode(children)
}else if(Array.isArray(children)){
childrenType = childrenTypes.many;
}else{
childrenType = childrenTypes.empty;
}
// 返回虚拟dom对象
return {
el:null,
type,
tag,
props,
children,
childrenType
}
}
//创建文本虚拟dom,直接返回一个对应的文本虚拟DOM
function createTextNode(text){
return {
type:'text',
tag:null,
props:null,
children:text,
childrenType:childrenTypes.empty
}
}
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
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
# 3、render()
// 渲染方法
function render(vnode, container){
if(container.vnode){
// 如果虚拟DOM已经存在,那么执行更新
// 这一步是相当复杂的diff算法,单独开辟章节来讲,此处暂时只考虑首次渲染
}else{
// 如果虚拟DOM没有存在,那么执行挂载(首次渲染)
mounted(vnode, container);
}
// 判断是初次渲染还是更新渲染
container.vnode = vnode;
}
// 首次渲染函数
function mounted(vnode,container){
let {type} = vnode;
if(type === 'HTML'){
// 渲染HTML元素方法
mountedElement(vnode,container)
}else{
// 渲染文本元素方法
mountedText(vnode,container)
}
}
// 渲染HTML元素的方法
function mountedElement( vnode, container ){
let { type, tag, props, children, childrenType } = vnode;
// el是真是的DOM元素,此处创建tag对应的DOM元素并赋给el
var el = document.createElement(tag);
vnode.el = el;
// 遍历设置props属性
if(props){
for(var key in props){
/* 设置DOM的属性(方法在代码最后)
语法:patchProps( 设置属性的元素, 属性的key值, 旧的value值, 新的value ) */
patchProps(el,key,null,props[key])
}
}
// 判断该元素的子元素的类型
if(childrenType === childrenTypes.single){
// 如果 childrenType 属性为 single 那么肯定是文本,用渲染文本方法将子元素渲染
mountedText(children, el)
}else if(childrenType === childrenTypes.many){
// 如果 childrenType 属性为 many 则肯定是嵌套子元素,遍历后用首次渲染方法将子元素渲染(递归)
children.forEach((item)=>{
mounted( item, el )
})
}
// 渲染完成后,最终要插入到父级里面(最高父级就是root)
container.appendChild(el);
}
// 渲染文本虚拟dom的方法
function mountedText(vnode,container){
// 创建一个对应的文本节点,直接插入父元素
var textNode = document.createTextNode(vnode.children);
vnode.el = textNode;
container.appendChild(textNode);
}
// 挂载属性的方法(部分情况)
// patchProps( 设置属性的元素, 属性的key值, 旧的value值, 新的value )
function patchProps(el, key, oldVal, newVal) {
switch (key) {
case 'className':
el.className = newVal;
break;
case 'id':
el.id = newVal;
break;
case 'onClick':
el.addEventListener("click", newVal);
break;
case 'style': {
for (var sKey in newVal) {
el.style[sKey] = newVal[sKey];
}
break;
}
default:
if (key != 'key') {
el.setAttribute(key, newVal);
}
}
}
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
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