目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DOM技术提高页面的渲染效率。那么,什么是Virtual DOM?它是通过什么方式去提升页面渲染效率的呢?
一、Virtual DOM是什么?
Virtual DOM,虚拟DOM,只是一个简单的JS对象,即用js对象去模拟dom结构。
为什么要模拟DOM结构?
如下:dom节点继承层次复杂,特别是ie中,自身的属性以及继承来的属性特别多,如下div上就有几百个属性
- 一个div上就几百个属性,其次dom继承层级比较复杂,特别是ie中。所以DOM 操作是非常“昂贵”的
- js对象对比特别快,毕竟js可以写后端,效率还是可以的,所以将 DOM 对比操作放在 JS 层,提高效率、提高重绘性能
通常用三个属性去描述dom结构,比如:标签名(tag)、属性(props)和子元素对象(children),不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。
<div>
Hello World!
<div id="div1" data-idx={1}>first</div>
<div id="div2">second</div>
</div>
vm对应结构如下:
{
"tag": "div",
"props":{},
"children": [
"Hello World!",
{
"tag": "div",
"props":
{
"id": "div1",
"data-idx": 1
},
"children": ["first"]
},
{
"tag": "div",
"props":
{
"id": "div2"
},
"children": ["second"]
}
]
}
二、Virtual DOM的必要性
如下案例:初次渲染render(data),点击change按钮后修改data数组中的数据,只是修改了两个,但是每次都是“推倒重来”=》?旧的dom结构删掉,插入新的dom结构
以下已经进行了优化,即一次性插入dom节点,而不是一行行tr插入
<body>
<div id="container"></div>
<div id="btn-change">change</div>
<script src="./jquery-3.2.1.js"></script>
<script>
var data = [
{
name: "张三",
age: 20,
address: "北京",
},
{
name: "李四",
age: 21,
address: "上海",
},
{
name: "王五",
age: 22,
address: "广州",
},
];
function render(data) {
var $container = $("#container");
$container.html("");
var $table = $("<table>");
$table.append($("<tr><td>name</td><td>age</td><td>address</td></tr>"));
data.forEach((element) => {
$table.append(
$(
`<tr><td>${element.name}</td><td>${element.age}</td><td>${element.address}</td></tr>`
)
);
});
$container.append($table);
}
$("#btn-change").click(function () {
data[1].age = 30;
data[2].address = "深圳";
render(data);
});
render(data);
</script>
</body>
思考:只是修改了两个数据,没必要推到重来,毕竟dom操作是很昂贵的,若只是更新变化的dom,那么性能会提升很多
snabbdom:https://github.com/snabbdom/snabbdom
<body>
<div id="container"></div>
<div id="btn-change">change</div>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-class.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-props.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-style.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/h.js"></script>
<script>
var snabbdom = window.snabbdom;
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners,
]);
var h = snabbdom.h;
var data = [
{
name: "姓名",
age: "年龄",
address: "地址",
},
{
name: "张三",
age: 20,
address: "北京",
},
{
name: "李四",
age: 21,
address: "上海",
},
{
name: "王五",
age: 22,
address: "广州",
},
];
var vnode;
var container = document.getElementById("container");
function render(data) {
var newVnode = h(
"table",
{},
data.map(function (item) {
var tds = [];
for (let i in item) {
if (item.hasOwnProperty(i)) {
tds.push(h("td", {}, item[i] + ""));
}
}
return h("tr", {}, tds);
})
);
if (vnode) {
patch(vnode, newVnode);
} else {
patch(container, newVnode);
}
vnode = newVnode;
}
document.getElementById("btn-change").onclick = function () {
data[1].age = 30;
data[2].address = "深圳";
render(data);
};
render(data);
</script>
</body>
如下当点击按钮时,只是更新了变化的dom而不像jquery那样“推倒重来”
Virtual DOM优点:
- Virtual DOM 最大的特点是将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。如 React 就借助 Virtual DOM 实现了服务端渲染、浏览器渲染和移动端渲染等功能。
- 在进行页面更新的时候,借助Virtual DOM,DOM 元素的改变可以在内存中进行比较,再结合框架的事务机制将多次比较的结果合并后一次性更新到页面,从而有效地减少页面渲染的次数,提高渲染效率。
如下页面的更新一般会经过几个阶段:
从上面可以看出页面的呈现会分以下3个阶段:
- JS计算
- 生成渲染树
- 绘制页面
JS计算用了935毫秒,生成渲染树143毫秒,绘制60毫秒。如果能有效的减少生成渲染树和绘制所花的时间,更新页面的效率也会随之提高。通过Virtual DOM的比较,我们可以将多个操作合并成一个批量的操作,从而减少dom重排的次数,进而缩短了生成渲染树和绘制所花的时间。
三、如何实现Virtual DOM与真实DOM的映射
借助JSX编译器,可以将文件中的HTML转化成函数的形式,然后再利用这个函数生成Virtual DOM。看下面这个例子:
//index.js
function view() {
return (
<div>
Hello World!
<div id="div1" data-ids="{1}">
first
</div>
<div id="div2">second</div>
</div>
);
}
通过babel的plugin(transform-react-jsx)编译后,可以生成如下代码:babel ./src/index.js -o ./public/vm_bundle.js(babel ./src/index.js --out-file ./public/vm_bundle.js)
//vm_bundle.js
function view() {
return h(
"div",
null,
"Hello World!",
h("div", { id: "div1", "data-ids": "{1}" }, "first"),
h("div", { id: "div2" }, "second")
);
}
这里的h是一个函数,可以起任意的名字-类似于snabbdom的h函数。这个名字通过babel进行配置:
// 安装npm包: npm install --save-dev babel-cli babel-plugin-transform-react-jsx
// babel-cli 是babel的命令行工具,需要将原始的 .js或.jsx 文件编译
// .babelrc
{
"plugins": [
[
"transform-react-jsx",
{
"pragma": "h"
}
]
]
}
接下来,只需要定义h函数,就能构造出Virtual DOM:
//h函数:生成vnode
function flatten(children) {
return [].concat.apply([], children);
}
function h(tag, props, ...children) {
return {
tag,
props: props || {},
children: flatten(children) || [],
};
}
初次渲染,生成vdom后,要基于Virtual DOM,生成真实的DOM--相当于实现snabbdom的patch函数:
//patch函数:vnode=》真实dom
function setProps(element, props) {
for (key in props) {
if (props.hasOwnProperty(key)) {
element.setAttribute(key, props[key]);
}
}
}
function createElement(vdom) {
const t = typeof vdom;
if (t === 'string' || t === 'number') {
return document.createTextNode(vdom);
}
const {tag, props, children} = vdom;
// 1. 创建元素
const element = document.createElement(tag);
// 2. 属性赋值
setProps(element, props);
// 3. 创建子元素
// appendChild在执行的时候,会检查当前的this是不是dom对象,因此要bind一下
children.map(createElement).forEach(element.appendChild.bind(element));
//children.map(createElement).forEach((child) => {
// element.appendChild(child);
//});
return element;
}
最后,将生成好的dom挂载到指定的节点上:
//真实dom插入页面
function renderDOM(vdom, container) {
container.appendChild(createElement(vdom));
}
renderDOM(view(), document.getElementById("root"));
如下可以通过一个html来显示最终成果:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app"></div>
<script src="vm_bundle.js"></script>
</body>
</html>
展示效果如下:
五、总结
本文介绍了Virtual DOM的基本概念,并讲解了如何利用JSX编译HTML标签,然后生成Virtual DOM,进而创建真实dom的过程。
项目源码:
https://github.com/qingye/VirtualDOM-Study/tree/master/VirtualDOM-Study-01