第二章 虚拟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 = propsprops = {}}// tagName, props, children数据保存到this对象上this.tagName = tagNamethis.props = props || {}this.children = children || []this.key = props ? props.key : undefinedlet count = 0this.children.forEach(child => {if (child instanceof CreateEl) {count += child.count} else {child = '' + child}count++})// 给每一个节点设置一个countthis.count = count}// 构建一个 dom 树render () {// 创建domconst 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 => {// 递归循环 构建treelet 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/
