vlambda博客
学习文章列表

第二章 虚拟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:子节点对象;


然后渲染到页面上。

<!doctype html><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树,然后插入到页面上。


实现的页面效果如下:

第二章 虚拟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/