Vue 之虚拟 DOM
介绍
为什么要学习虚拟 DOM
尤大推荐,Vue.js 在用。
什么是虚拟 DOM
就是用 JS 去按照 DOM 结构来实现的树形结构对象,你也可以叫做 DOM 对象
比如:下边的 HTML 结构可以使用 JavaScript 对象的形式表达出来。
HTMl
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>
2
3
4
5
JavaScript
var element = {
tagName: "ul", // 节点标签名
props: {
// DOM的属性,用一个对象存储键值对
id: "list",
},
children: [
// 该节点的子节点
{ tagName: "li", props: { class: "item" }, children: ["Item 1"] },
{ tagName: "li", props: { class: "item" }, children: ["Item 2"] },
{ tagName: "li", props: { class: "item" }, children: ["Item 3"] },
],
};
2
3
4
5
6
7
8
9
10
11
12
13
然后在通过createElement
等 DOM API 操作完成从 JavaScript 对象=》真实 DOM 的转化。
为什么要用虚拟 DOM?
- 操作真实 DOM 花销很大,引起重绘、回流等
- 减少不必要的开销比如三个列表,比如现在替换一种一个列表,通过 diff 后,只需要操作一次列表(局部变动),这样可以提升性能。
- 其他的比如跨平台(RN、Weex、Flutter),组件的高度抽象化、可以更好的实现 SSR、同构渲染等
实现一个虚拟 DOM
使用 JavaScript 的对象结构去描述一个 DOM,然后渲染到页面上。 第一步:得有一个创建虚拟 DOM 的方法(称为 createElement) 第二步:通过虚拟 DOM 生成真实 DOM 的方法(render) 第三步:将生成后的真实 DOM 插入页面中(renderDom)
创建虚拟 DOM
先来了解一个基本的 DOM 的组成:
<ul id="list">
<li class="item">Item 1</li>
</ul>
2
3
分别对应三个属性:
- type:
div
- props:
id="list"
- children:
{type: "li",props:{class:"item"},children:"Item 1"}
明确了属性和值,我们可以借助构造函数去批量的创建虚拟 DOM 了,
// element.js
class Element {
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}
2
3
4
5
6
7
8
createElement
函数,用于将创建后的虚拟 DOM 返回
// createElement.js
function createElement(type, props, children) {
return new Element(type, props, children);
}
2
3
4
生成真实 DOM
通过一个 render 函数将 vDOM 转化为真实的 DOM。
// render.js
// render 方法可以将虚拟DOM转化为真是的DOM
function render(vDom) {
let { type, props, children } = vDom;
// 创建元素
let el = document.createElement(type);
// 遍历props,设置属性
for (let key in props) {
setAttr(el, key, props[key]);
}
// 遍历子节点
// 如果是虚拟DOM,就递归
// 不是就是文本节点,直接创建
children.forEach((childEl) => {
childEl =
childEl instanceof Element
? render(childEl)
: document.createTextNode(childEl);
el.appendChild(childEl);
});
// 创建完毕返回节点
return el;
}
// 设置属性
function setAttr(node, key, value) {
if (key === "value") {
// 如果是value,则表明是input、textarea节点
let tagName = node.tagName.toLowerCase();
if (tagName == "input" || tagName == "textarea") {
node.value = value;
} else {
node.setAttribute(key, value);
}
} else if (key === "style") {
// 直接设置的行内样式
node.style.cssText = value;
} else {
// 设置的属性 比如class、id、dataset等
node.setAttribute(key, value);
}
}
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
通过 renderDom 将生成后的真实 DOM 挂载到制定的元素上
// renderDom.js
// 将生成的真实DOM,添加到目标元素中
function renderDom(el, target) {
target.appendChild(el);
}
2
3
4
5
使用
接下来,就是见证奇迹的时刻了
// index.js
// 描述虚拟DOM的JS对象
let jsDomObj = {
type: "ul",
props: {
class: "list",
},
children: [
createElement(
"li",
{
class: "item",
},
["周杰伦"]
),
createElement(
"li",
{
class: "item",
},
["林俊杰"]
),
createElement(
"li",
{
class: "item",
},
["王力宏"]
),
],
};
// 通过createElement将JS对象转化为虚拟DOM
let virtualDom = createElement(
jsDomObj.type,
jsDomObj.props,
jsDomObj.children
);
// 打印 虚拟DOM
console.log("虚拟DOM\n", virtualDom);
// 渲染虚拟DOM得到真实的DOM结构
let el = render(virtualDom);
// 打印 真实DOM
console.log("真实DOM\n", el);
// 直接将DOM添加到页面内
renderDom(el, document.getElementById("root"));
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
打开页面后,发现了 list 渲染成功!! 我们可以查看控制台打印的结果:
Diff 算法
这就是所谓的 Virtual DOM 算法。包括几个步骤:
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中当状态变更的时候,重新构造一棵新的对象树。
- 然后用新的树和旧的树进行比较,记录两棵树差异把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了 Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。
可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。
结语
Vue 编译也分两个版本
- 一个是运行时+编译器,比如使用 vue-cli,是通过 webpack 的 vue-loader 将模板编译成真实 DOM 的
- 一个是运行时,比如直接使用 CDN 将整个 Vue 代码引入到项目当中,则使用 render 方法编译的。
Vue.js 使用的 Virtual DOM 参考的是开源库 github-snabbdomopen in new window 有兴趣的可以看一下他的源码。
感谢Matt-Esch/virtual-domopen in new window项目,带来的帮助。
完整代码地址:https://github.com/AnsonZnl/virtual-dom-study