手撕Virtual-DOM

JS操作真实DOM的代价

用我们传统的开发模式,原生JS或JQ操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。例如,第一次计算完,紧接着下一个DOM更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算DOM节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验。

为什么需要虚拟DOM,它有什么好处?

Web界面由DOM树(树的意思是数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM节点发生了变化,

虚拟DOM就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。

虚拟DOM 的作用

要想回答上面那个问题,真的不要仅仅以为虚拟 DOM 或者 React 是来解决性能问题的,好处可还有很多呢。下面我总结了一些虚拟 DOM 好作用。

  • Virtual DOM 在牺牲(牺牲很关键)部分性能的前提下,增加了可维护性,这也是很多框架的通性。
  • 实现了对 DOM 的集中化操作,在数据改变时先对虚拟 DOM 进行修改,再反映到真实的 DOM中,用最小的代价来更新DOM,提高效率(提升效率要想想是跟哪个阶段比提升了效率,别只记住了这一条)
  • 打开了函数式 UI 编程的大门。
  • 可以渲染到 DOM 以外的端,使得框架跨平台,比如 ReactNative,React VR 等。
  • 可以更好的实现 SSR,同构渲染等。这条其实是跟上面一条差不多的。
  • 组件的高度抽象化。

既然虚拟 DOM 有这么多作用,那么上面的问题,Vue 采用虚拟 DOM 的原因是什么呢?

Vue 2.0 引入 vdom 的主要原因是 vdom 把渲染过程抽象化了,从而使得组件的抽象能力也得到提升,并且可以适配 DOM 以外的渲染目标。 Vue 的理念问题

虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种 GUI。

虚拟 DOM 的缺点

  • 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。
  • 虚拟 DOM 需要在内存中的维护一份 DOM 的副本(更上面一条其实也差不多,上面一条是从速度上,这条是空间上)。
  • 如果虚拟 DOM 大量更改,这是合适的。但是单一的,频繁的更新的话,虚拟 DOM 将会花费更多的时间处理计算的工作。所以,如果你有一个DOM 节点相对较少页面,用虚拟 DOM,它实际上有可能会更慢。但对于大多数单页面应用,这应该都会更快。
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
const $app = document.querySelector('.app');

function dom(type, props, ...children) {
return { type, props, children };
}

function generateDom(domObj) {
let el;

if (domObj.type) {
el = document.createElement(domObj.type);
} else {
el = document.createTextNode(domObj);
}

if (domObj.props) {
Object.keys(domObj.props).forEach((key) => {
el.setAttribute(key, domObj.props[key]);
});
}

if (domObj.children) {
domObj.children.forEach(child => el.appendChild(generateDom(child)));
}

return el;
}

const types = {
get: type => Object.prototype.toString.call(type),
string: '[object String]',
number: '[object Number]',
array: '[object Array]',
object: '[object Object]',
function: '[object Function]',
null: '[object Null]',
undefined: '[object Undefined]',
boolean: '[object Boolean]',
};

function isObjChanged(obj1, obj2) {
// Different data types
if (types.get(obj1) !== types.get(obj2)) {
return true;
}

// Diff objects
if (types.get(obj1) === types.object) {
const obj1Keys = Object.keys(obj1);
const obj2Keys = Object.keys(obj1);

if (obj1Keys.length !== obj2Keys.length) {
return true;
}

// Empty object as no change
if (obj1Keys.length === 0) {
return false;
}

// Compare each item of objects
for (let i = 0; i < obj1Keys.length; i++) {
let key = obj1Keys[i];

if (obj1[key] !== obj2[key]) {
return true;
}
}
}

return false;
}

function isNodeChanged(dom1, dom2) {
if (types.get(dom1) === types.object && types.get(dom2) === types.object) {
return dom1.type !== dom2.type;
}

return dom1 !== dom2;
}

function updateState($parent, oldNode, newNode, index = 0) {
// Clear empty text node
if ($parent.childNodes.length === 1 &&
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
$parent.childNodes[0].nodeType === 3 &&
$parent.childNodes[0].data.trim() === ''
) {
$parent.removeChild($parent.childNodes[0]);
}

const $currentNode = $parent.childNodes[index];

// No new node, remove old node
if (!newNode) {
return $parent.removeChild($currentNode);
}

// No old node, append newNode
if (!oldNode) {
return $parent.appendChild(generateDom(newNode));
}

// node.type or string change
if (isNodeChanged(oldNode, newNode)) { // type 不一样直接替换
return $parent.replaceChild(
generateDom(newNode),
$currentNode
);
}

// old and new are the same string
if (oldNode === newNode) {
return;
}

// prop change
if (isObjChanged(oldNode.props, newNode.props)) {
const oldProps = oldNode.props || {};
const newProps = newNode.props || {};
const oldPropsKeys = Object.keys(oldProps);
const newPropsKeys = Object.keys(newProps);

// New props is null, delete all old.
if (newPropsKeys.length === 0) {
oldPropsKeys.forEach((prop) => {
$currentNode.removeAttribute(prop);
});
} else {
const allPropsKeys = new Set([...oldPropsKeys, ...newPropsKeys]);

allPropsKeys.forEach((prop) => {
// No old prop, set new
if (oldProps[prop] === undefined) {
return $currentNode.setAttribute(prop, newProps[prop]);
}

// No new prop, remove old.
if (newProps[prop] === undefined) {
return $currentNode.removeAttribute(prop);
}

// Diff value
if (oldProps[prop] !== newProps[prop]) {
return $currentNode.setAttribute(prop, newProps[prop]);
}
});
}
}

// diff children.
if ((oldNode.children && oldNode.children.length)
|| (newNode.children && newNode.children.length)
) {
let maxLength = Math.max(oldNode.children.length, newNode.children.length);

for (let i = 0; i < maxLength; i++) {
updateState( // 递归比较
$currentNode,
oldNode.children[i],
newNode.children[i],
i
);
}
}
}

const activeProfile = <div class="main-01">
<div class="profile" id="profile">
<p>虚拟DOM</p>
<h3>虚拟DOM2</h3>
</div>
</div>;


console.log(activeProfile);



const inactiveProfile = <div class="main-02 test">
<div class="profile" data-user-name="Tonni">
<h1>Virtual DOM</h1>
<p>虚拟DOM2</p>
</div>
</div>;



updateState($app, null, activeProfile);

const $updateDom = document.querySelector('.update-dom');

$updateDom.addEventListener('click', (e) => {
updateState($app, activeProfile, inactiveProfile);
});
1
2
3
4
5
6
7
8
9
const activeProfile = <div class="main-01">
<div class="profile" id="profile">
<p>虚拟DOM</p>
<h3>虚拟DOM2</h3>
</div>
</div>;


console.log(activeProfile);

image

再次审视Virtual DOM,可以简单得出如下结论:

  • Virtual DOM 在牺牲部分性能的前提下,增加了可维护性,这也是很多框架的通性
  • 实现了对DOM的集中化操作,在数据改变时先对虚拟DOM进行修改,再反映到真实的DOM中,用最小的代价来更新DOM,提高效率
  • 打开了函数式UI编程的大门
  • 可以渲染到DOM以外的端,比如ReactNative

根据 React 历史来聊如何理解虚拟 DOM

vue核心之虚拟DOM(vdom)

虚拟DOM-virtual-dom

medium Blog