目前最流行的两大前端框架,React 和 Vue,都不约而同的借助 Virtual DOM 技术提升页面的渲染效率。那么,什么是 Virtual DOM ?它是经过什么方式去提高页面渲染效率的呢?本系列文章会详细讲解 Virtual DOM 的建立过程,并实现一个简单的 Diff 算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的 Virtual DOM 。敲单词太累了,下文 Virtual DOM 一概用 VD 表示。javascript
这是 VD 系列文章的开篇,后续还会有更多的文章带你深刻了解 VD 的奥秘。前端
本质上来讲,VD 只是一个简单的JS对象,而且最少包含tag
、props
和children
三个属性。不一样的框架对这三个属性的命名会有点差异,但表达的意思是一致的。它们分别是标签名( tag )、属性( props )和子元素对象( children )。下面是一个典型的 VD 对象例子:java
{
tag: "div",
props: {},
children: [
"Hello World",
{
tag: "ul",
props: {},
children: [{
tag: "li",
props: {
id: 1,
class: "li-1"
},
children: ["第", 1]
}]
}
]
}
复制代码
VD 跟 dom 对象有一一对应的关系,上面的 VD 是由如下的 HTML 生成的react
<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])
三个属性来描述。git
借助 VD,能够达到有效减小页面渲染次数的目的,从而提升渲染效率。咱们先来看下页面的更新通常会通过几个阶段。github
从上面的例子中,能够看出页面的呈现会分如下3个阶段:算法
这个例子里面,JS 计算用了691
毫秒,生成渲染树578
毫秒,绘制73
毫秒。若是能有效的减小生成渲染树和绘制所花的时间,更新页面的效率也会随之提升。 经过 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 编译后,会输出下面的内容:babel
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.: 想看完整代码见这里:代码