虚拟DOM
# 虚拟DOM
[TOC]
# 一、虚拟DOM
# 1.1 是什么
用JS模拟DOM结构,提高性能。
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
</ul>
1
2
3
4
2
3
4
用 JS 来模拟上面的 DOM 结构
{
tag: 'ul',
attrs: {
id: 'list'
},
children: [
{
tag: 'li',
attrs: { className: 'item' },
children: ['Item 1']
}, {
tag: 'li',
attrs: { className: 'item' },
children: ['Item 2']
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用来提高重绘性能
- 浏览器执行DOM操作是最昂贵的。
// 查看一个普通div的属性有多少 var div = document.createElement('div'); var arr = []; for(var key in div){arr.push(key)}; console.log(arr); // 241
1
2
3
4
5- 执行js一万次都不要执行一次DOM,js运行效率高。
# 1.2 snabbdom - 实现虚拟DOM的库
// {}:存放的是属性
var vnode = h('ul#list',{},[
h('li.item',{},'Item 1'),
h('li.item',{},'Item 2')
]);
// 首次渲染
var container = document.getElementById('container');
patch(container, vnode);
// 模拟改变
var btnChange = document.getElementById('btn-change');
btnChange.addEventListener('click', function(){
var newNode = h('ul#list',{},[
h('li.item',{},'Item 31'),
h('li.item',{},'Item 32'),
h('li.item',{},'Item 33'),
]);
// 比较后再渲染
patch(vnode, newNode);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 二、算法实现
# 2.1 用JS对象模拟DOM树
# 2.1.1 JS记录DOM节点(标签,属性,子节点)
class Element {
constructor(tagName, props, children) {
this.tagName = tagName;
this.props = props;
this.children = children;
}
}
function el(tagName, props, children){
return new Element(tagName, props, children);
}
var ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.1.2 构建真正的DOM节点
class Element {
render() {
// 1.根据tagName构建
var ele = document.createElement(this.tagName);
var props = this.props;
// 2.设置节点的DOM属性
for (var propName in props) {
var propValue = props[propName];
ele.setAttribute(propName, propValue);
}
// 3.递归构建子节点
var children = this.children || [];
children.forEach(function (child) {
var childEle = (child instanceof Element)
?child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
:document.createTextNode(child) // 字符串则构建文本节点
ele.appendChild(childEle);
});
return ele;
}
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 2.2 Diff算法
- 找出需要更新的节点,只对同一个层级的元素进行对比。
let index = 0; // 当前比较节点的索引
function diff(oldTree, newTree) {
// 存放所有节点的补丁
let patches = {};
// 递归树 比较后的结果放到补丁里
dfsWalk(oldTree, newTree, index, patches);
return patches;
};
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 2.2.1 比较规则
function dfsWalk(oldNode, newNode, index, patches) {
// 存放当前节点的补丁
let currentPatch = [];
// rule1:新节点为null:{type: 'REMOVE', index}
if (newNode === null) {
currentPatch.push({
type: 'REMOVE',
index
});
}
// rule2:文本节点不一致:{ type: 'TEXT', text: newNode }
else if (typeof oldNode === 'string' && typeof newNode === 'string') {
if (oldNode !== newNode) {
currentPatch.push({
type: 'TEXT',
text: newNode
});
}
}
// rule3: 节点类型相同而属性不同:{type: 'ATTR', attr: {className: 'list-group'}}
else if (oldNode.tagName === newNode.tagName) {
let attrPatch = diffAttr(oldNode.props, newNode.props);
if (Object.keys(attrPatch).length) {
currentPatch.push({
type: 'ATTR',
attr: attrPatch
});
}
// 如果有子节点,遍历子节点
diffChildren(oldNode.children, newNode.children, patches);
}
// rule4: 节点类型不相同{type: 'REPLACE', newNode}
else {
currentPatch.push({
type: 'REPLACE',
newNode: newNode
});
}
if (currentPatch.length) {
patches[index] = currentPatch;
}
};
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
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
# 2.2.2 属性比较和子节点遍历
function diffAttr(oldAttrs, newAttrs) {
// 存放属性补丁
let attrPatch = {};
// 旧节点已被改变的属性
for (let key in oldAttrs) {
if (oldAttrs[key] !== newAttrs[key]) {
// 有可能是undefined,即新节点没有该属性
attrPatch[key] = newAttrs[key];
}
}
// 新节点添加的属性
for (let key in newAttrs) {
if (!oldAttrs.hasOwnProperty(key)) {
attrPatch[key] = newAttrs[key];
}
}
return attrPatch;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function diffChildren(oldChildren, newChildren, patches) {
oldChildren.forEach((child, idx) => {
dfsWalk(child, newChildren[idx], ++index, patches);
});
}
1
2
3
4
5
2
3
4
5
# 2.3 把差异应用到真正的DOM树上
进行patch补丁更新。
# patch步骤
- patch方法接收两个参数(node, patches) ,内部调用dfsPatchWalk方法,给某个元素打上补丁。
let index = 0; function patch(node, patches) { dfsPatchWalk(node, patches); };
1
2
3
4- dfsPatchWalk方法里获取所有的子节点
- 给子节点也进行先序深度优先遍历,递归dfsPatchWalk。
- 如果当前的补丁是存在的,那么就对其打补丁(doPatch)。
function dfsPatchWalk(node, patches) { let currentPatches = patches[index++]; let childNodes = node.childNodes; // 先序深度遍历,继续遍历递归子节点 childNodes.forEach(child => dfsPatchWalk(child, patches)); if (currentPatches) { // 对当前节点进行补丁 doPatch(node, currentPatches); } }
1
2
3
4
5
6
7
8
9
10
11
12- doPatch打补丁方法会根据传递的patches进行遍历 。
- 属性ATTR:for in遍历attrs对象,对应的key值存在就设置属性setAttr; 不存在就删除对应属性。
- 文字TEXT:补丁的text赋值给node节点的textContent。
- 替换REPLACE:新节点是Element的实例,就调用render方法;否则新节点是文本节点,创建一个文本节点。通过调用父级parentNode的replaceChild方法替换为新的节点。
- 删除REMOVE:调用父级的removeChild方法删除节点。
function doPatch(node, patches) { // 遍历所有打过的补丁 patches.forEach(patch => { switch (patch.type) { case 'ATTR': for (let key in patch.attr) { let value = patch.attr[key]; if (value) { node.setAttribute(key, value); } else { node.removeAttribute(key); } } break; case 'TEXT': node.textContent = patch.text; break; case 'REPLACE': let newNode = patch.newNode; newNode = (newNode instanceof Element) ? newNode.render() : document.createTextNode(newNode); node.parentNode.replaceChild(newNode, node); break; case 'REMOVE': node.parentNode.removeChild(node); break; default: break; } }); }
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测试用例
// 1. 构建虚拟DOM var tree = el('div', {'id': 'container'}, [ el('h1', {style: 'color: blue'}, ['simple virtal dom']), el('h2', {style: 'color: blue'}, ['Hello, virtual']), el('ul', {style: 'color: blue'}, [el('li',{},[''])]) ]) // 2. 通过虚拟DOM构建真正的DOM var root = tree.render() document.body.appendChild(root) // 3. 生成新的虚拟DOM var newTree = el('div', {'id': 'container'}, [ el('h1', {style: 'color: red'}, ['simple virtal dom']), el('h2', {style: 'color: blue'}, ['Hello, virtual-dom']), el('ul', {style: 'color: red'}, [el('li',{},['haha'])]) ]) // 4. 比较两棵虚拟DOM树的不同 var patches = diff(tree, newTree); // 5. 在真正的DOM元素上应用变更 patch(root, patches);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23