第二章 虚拟DOM以及其实现原理
“ 在网页浏览器只能够资源开销最大便是DOM节点,DOM很慢并且非常庞大,网页性能问题大多数都是JavaScript修改DOM引起。”
DOM是浏览器中的概念,用JS对象来表示页面上都元素,并提供了操作DOM对象都API;而React中的虚拟DOM是框架之中的概念,是程序员用JavaScript对象来模拟页面上的DOM和DOM嵌套;虚拟DOM都目的就是为了实现页面中DOM元素的高效更新;
01 虚拟DOM(Virtual Document Object Model)
—
传统渲染数据到页面的两种方案:
手动循环整个数组,然后手动拼接字符串来完成;
使用模板引擎,art-template或者jsrender;
但是上述的方案有着性能上的缺陷,如果用户想按照时间进行排序,首先要触发事件把内存中的对象数组,重新排序,当排序之后,页面是旧的,但是内存中对象数组是新的,想办法把最新的数组,重新渲染到页面上。
分析与总结:上述方案只是实现了把数据渲染到页面上,但是并没有把性能做到最优,我们需要做到按需渲染页面,只需要重新渲染更新数据对应到页面内容。
一个页面呈现的过程
1、浏览器请求服务器获取页面HTML代码;
2、浏览器要在内存中解析DOM结构,并在浏览器内存中,渲染出一颗DOM树;
3、浏览器把DOM树,呈现到页面上;
02 虚拟DOM实现原理
—
虚拟DOM的实现原理主要包括了以下三个部分:
用JavaScript对象模拟真实DOM树,对真实DOM进行抽象;
diff算法:比较两颗虚拟DOM树的差异;
pach算法:将两个虚拟DOM对象的差异应用到真正到DOM树;
DOM树上的结构信息,我们可以使用JavaScript对象很容易的就表示出来。例如我们要表现如下的HTML结构。
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul
我们可以使用如下的JSOn对象来表示
var element = {
tagName: 'ul',
props: {
id: 'list'
},
children: [
{
tagName: 'li',
props: {class: 'item'},
children: ['Item1']
},
{
tagName: 'li',
props: {class: 'item'},
children: ['Item1']
},
{
tagName: 'li',
props: {class: 'item'},
children: ['Item1']
}
]
}
tagName:用来表示这个元素的标签名;
props:用来表示这元素所包含的属性;
childrend:用来表示这元素的children。
1、用JavaScript对象模拟真实DOM树,对真实DOM进行抽象;
所谓虚拟DOM就是利用JS对象结构对一种映射。我们用JavaScript很容易就能模拟一个DOM树对结构,例如我们使用这样对函数createEl(tagName, props, children)来创建DOM结构。
tagName:标签名;
props:属性的对象;
children:子节点对象;
然后渲染到页面上。
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>第一天 React虚拟Dom实现</title>
</head>
<body>
<script>
function setAttr(node, key, value) {
switch (key) {
case "value":
if (node.tagName.toUpperCase() === "INPUT" ||
node.tagName.toUpperCase() === "TEXTAREA") {
node.value = value;
}
break;
case "style":
node.style.cssText = value;
break;
default:
node.setAttribute(key, value);
}
}
class CreateEl {
constructor (tagName, props, children) {
// 当只有两个参数的时候 例如 celement(el, [123])
if (Array.isArray(props)) {
children = props
props = {}
}
// tagName, props, children数据保存到this对象上
this.tagName = tagName
this.props = props || {}
this.children = children || []
this.key = props ? props.key : undefined
let count = 0
this.children.forEach(child => {
if (child instanceof CreateEl) {
count += child.count
} else {
child = '' + child
}
count++
})
// 给每一个节点设置一个count
this.count = count
}
// 构建一个 dom 树
render () {
// 创建dom
const el = document.createElement(this.tagName)
const props = this.props
// 循环所有属性,然后设置属性
for (let [key, val] of Object.entries(props)) {
setAttr(el, key, val)
}
this.children.forEach(child => {
// 递归循环 构建tree
let childEl = (child instanceof CreateEl) ? child.render() : document.createTextNode(child)
el.appendChild(childEl)
})
return el
}
}
const createEl = (tagName, props, children) => new CreateEl(tagName, props, children);
const vdom = createEl('div', { 'id': 'box' }, [
createEl('h1', { style: 'color: pink' }, ['I am H1']),
createEl('ul', {class: 'list'}, [createEl('li', ['#list1']), createEl('li', ['#list2'])]),
createEl('p', ['I am p'])
]);
const rootnode = vdom.render();
document.body.appendChild(rootnode);
</script>
</body>
</html>
上面render函数的功能,就是把节点创建好,然后设置节点属性,最后递归创建。这样子我们就得到一个DOM树,然后插入到页面上。
实现的页面效果如下:
2、Diff算法:比较新老DOM树,得到比较的差异对象;
两颗DOM树的差异是虚拟DOM的最核心的部分,这也是人们常说的diff算法,两颗完全的树差异比较的时间复杂度为O(n^3)。但是我们的Web中很少用到跨层级DOM树的比较,所以一个层级跟一个层级的对比,这样算法复杂度就可以达到O(n)。
两个节点之间的差异有如下几种情况:
直接替换原节点
调整子节点,包括移动、删除等;
修改节点属性
修改节点文本内容
2.1、深度遍历优先,记录差异
在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每一个节点就会有一个唯一的标记:
在深度优先遍历的时候,每遍历到一个节点就把该节点和新的树进行对比,如果有差异就记录到一个对象里面。
由于本人JS代码比较粗糙,就不献丑了。
3、patch算法:
这一部分,就是我们把diff算法求出来的差异,引用到真正到DOM树上。
有兴趣到朋友可以看看别人github实现到简单版代码,这里我就不做实现了。
https://github.com/livoras/simple-virtual-dom/