一、前言
目前最流行的两大前端框架,React 和 Vue,都不约而同的借助 Virtual DOM 技术提高页面的渲染效率。那么,什么是 Virtual DOM ?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解 Virtual DOM 的创建过程,并实现一个简单的 Diff 算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的 Virtual DOM 。敲单词太累了,下文 Virtual DOM 一律用 VD 表示。
这是 VD 系列文章的开篇,后续还会有更多的文章带你深入了解 VD 的奥秘。
二、VD 是什么
本质上来说,VD 只是一个简单的JS对象,并且最少包含tag
、props
和children
三个属性。不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。它们分别是标签名( tag )、属性( props )和子元素对象( children )。下面是一个典型的 VD 对象例子:
{
tag: "div",
props: {},
children: [
"Hello World",
{
tag: "ul",
props: {},
children: [{
tag: "li",
props: {
id: 1,
class: "li-1"
},
children: ["第", 1]
}]
}
]
}
VD 跟 dom 对象有一一对应的关系,上面的 VD 是由以下的 HTML 生成的
<div>
Hello World
<ul>
<li id="1" class="li-1">
第1
</li>
</ul>
</div>
一个 dom 对象,比如li
,由tag(li)
, props({id: 1, class: "li-1"})
和children(["第", 1])
三个属性来描述。
三、为什么需要 VD
借助 VD,可以达到有效减少页面渲染次数的目的,从而提高渲染效率。我们先来看下页面的更新一般会经过几个阶段。
从上面的例子中,可以看出页面的呈现会分以下3个阶段:
- JS计算
- 生成渲染树
- 绘制页面
这个例子里面,JS 计算用了691
毫秒,生成渲染树578
毫秒,绘制73
毫秒。如果能有效的减少生成渲染树和绘制所花的时间,更新页面的效率也会随之提高。
通过 VD 的比较,我们可以将多个操作合并成一个批量的操作,从而减少 dom 重排的次数,进而缩短了生成渲染树和绘制所花的时间。至于如何基于 VD 更有效率的更新 dom,是一个很有趣的话题,日后有机会将另写一篇文章介绍。
四、如何实现 VD 与真实 DOM 的映射
我们先从如何生成 VD 说起。借助 JSX 编译器,可以将文件中的 HTML 转化成函数的形式,然后再利用这个函数生成 VD。看下面这个例子:
function render() {
return (
<div>
Hello World
<ul>
<li id="1" class="li-1">
第1
</li>
</ul>
</div>
);
}
这个函数经过 JSX 编译后,会输出下面的内容:
function render() {
return h(
'div',
null,
'Hello World',
h(
'ul',
null,
h(
'li',
{ id: '1', 'class': 'li-1' },
'\u7B2C1'
)
)
);
}
这里的h是一个函数,可以起任意的名字。这个名字通过 babel 进行配置:
// .babelrc 文件
{
"plugins": [
["transform-react-jsx", {
"pragma": "h" // 这里可配置任意的名称
}]
]
}
接下来,我们只需要定义 h 函数,就能构造出 VD
function flatten(arr) {
return [].concat.apply([], arr);
}
function h(tag, props, ...children) {
return {
tag,
props: props || {},
children: flatten(children) || []
};
}
h 函数会传入三个或以上的参数,前两个参数一个是标签名,一个是属性对象,从第三个参数开始的其它参数都是 children。children 元素有可能是数组的形式,需要将数组解构一层。比如:
function render() {
return (
<ul>
<li>0</li>
{
[1, 2, 3].map( i => (
<li>{i}</li>
))
}
</ul>
);
}
// JSX 编译后
function render() {
return h(
'ul',
null,
h(
'li',
null,
'0'
),
/*
* 需要将下面这个数组解构出来再放到 children 数组中
*/
[1, 2, 3].map(i => h(
'li',
null,
i
))
);
}
继续之前的例子。执行 h 函数后,最终会得到如下的 VD 对象:
{
tag: "div",
props: {},
children: [
"Hello World",
{
tag: "ul",
props: {},
children: [{
tag: "li",
props: {
id: 1,
class: "li-1"
},
children: ["第", 1]
}]
}
]
}
下一步,通过遍历 VD 对象,生成真实的 dom
// 创建 dom 元素
function createElement(vdom) {
// 如果 vdom 是字符串或者数字类型,则创建文本节点,比如“Hello World”
if (typeof vdom === 'string' || typeof vdom === 'number') {
return doc.createTextNode(vdom);
}
const {tag, props, children} = vdom;
// 1. 创建元素
const element = doc.createElement(tag);
// 2. 属性赋值
setProps(element, props);
// 3. 创建子元素
// appendChild 在执行的时候,会检查当前的 this 是不是 dom 对象,因此要 bind 一下
children.map(createElement)
.forEach(element.appendChild.bind(element));
return element;
}
// 属性赋值
function setProps(element, props) {
for (let key in props) {
element.setAttribute(key, props[key]);
}
}
createElement
函数执行完后,dom元素就创建完并展示到页面上了(页面比较丑,不要介意...)。
五、总结
本文介绍了 VD 的基本概念,并讲解了如何利用 JSX 编译 HTML 标签,然后生成 VD,进而创建真实 dom 的过程。下一篇文章将会实现一个简单的 VD Diff 算法,找出 2 个 VD 的差异并将更新的元素映射到 dom 中去。
P.S.: 想看完整代码见这里:代码