DOM是很慢的,其元素很是庞大,页面的性能问题鲜有由JS引发的,大部分都是由DOM操做引发的。若是对前端工做进行抽象的话,主要就是维护状态和更新视图;而更新视图和维护状态都须要DOM操做。其实近年来,前端的框架主要发展方向就是解放DOM操做的复杂性。javascript
在jQuery出现之前,咱们直接操做DOM结构,这种方法复杂度高,兼容性也较差;有了jQuery强大的选择器以及高度封装的API,咱们能够更方便的操做DOM,jQuery帮咱们处理兼容性问题,同时也使DOM操做变得简单;可是聪明的程序员不可能知足于此,各类MVVM框架应运而生,有angularJS、avalon、vue.js等,MVVM使用数据双向绑定,使得咱们彻底不须要操做DOM了,更新了状态视图会自动更新,更新了视图数据状态也会自动更新,能够说MMVM使得前端的开发效率大幅提高,可是其大量的事件绑定使得其在复杂场景下的执行性能堪忧;有没有一种兼顾开发效率和执行效率的方案呢?ReactJS就是一种不错的方案,虽然其将JS代码和HTML代码混合在一块儿的设计有很多争议,可是其引入的Virtual DOM(虚拟DOM)倒是获得你们的一致认同的。前端
虚拟的DOM的核心思想是:对复杂的文档DOM结构,提供一种方便的工具,进行最小化地DOM操做。这句话,也许过于抽象,却基本概况了虚拟DOM的设计思想vue
(1) 提供一种方便的工具,使得开发效率获得保证 (2) 保证最小化的DOM操做,使得执行效率获得保证
DOM很慢,而javascript很快,用javascript对象能够很容易地表示DOM节点。DOM节点包括标签、属性和子节点,经过VElement表示以下。java
//虚拟dom,参数分别为标签名、属性对象、子DOM列表 var VElement = function(tagName, props, children) { //保证只能经过以下方式调用:new VElement if (!(this instanceof VElement)) { return new VElement(tagName, props, children); } //能够经过只传递tagName和children参数 if (util.isArray(props)) { children = props; props = {}; } //设置虚拟dom的相关属性 this.tagName = tagName; this.props = props || {}; this.children = children || []; this.key = props ? props.key : void 666; var count = 0; util.each(this.children, function(child, i) { if (child instanceof VElement) { count += child.count; } else { children[i] = '' + child; } count++; }); this.count = count; }
经过VElement,咱们能够很简单地用javascript表示DOM结构。好比node
var vdom = velement('div', { 'id': 'container' }, [ velement('h1', { style: 'color:red' }, ['simple virtual dom']), velement('p', ['hello world']), velement('ul', [velement('li', ['item #1']), velement('li', ['item #2'])]), ]);
上面的javascript代码能够表示以下DOM结构:程序员
<div id="container"> <h1 style="color:red">simple virtual dom</h1> <p>hello world</p> <ul> <li>item #1</li> <li>item #2</li> </ul> </div>
一样咱们能够很方便地根据虚拟DOM树构建出真实的DOM树。具体思路:根据虚拟DOM节点的属性和子节点递归地构建出真实的DOM树。见以下代码:算法
VElement.prototype.render = function() { //建立标签 var el = document.createElement(this.tagName); //设置标签的属性 var props = this.props; for (var propName in props) { var propValue = props[propName] util.setAttr(el, propName, propValue); } //依次建立子节点的标签 util.each(this.children, function(child) { //若是子节点仍然为velement,则递归的建立子节点,不然直接建立文本类型节点 var childEl = (child instanceof VElement) ? child.render() : document.createTextNode(child); el.appendChild(childEl); }); return el; }
对一个虚拟的DOM对象VElement,调用其原型的render方法,就能够产生一颗真实的DOM树。数据结构
vdom.render();
既然咱们能够用JS对象表示DOM结构,那么当数据状态发生变化而须要改变DOM结构时,咱们先经过JS对象表示的虚拟DOM计算出实际DOM须要作的最小变更,而后再操做实际DOM,从而避免了粗放式的DOM操做带来的性能问题。app
在用JS对象表示DOM结构后,当页面状态发生变化而须要操做DOM时,咱们能够先经过虚拟DOM计算出对真实DOM的最小修改量,而后再修改真实DOM结构(由于真实DOM的操做代价太大)。框架
以下图所示,两个虚拟DOM之间的差别已经标红:
为了便于说明问题,我固然选取了最简单的DOM结构,两个简单DOM之间的差别彷佛是显而易见的,可是真实场景下的DOM结构很复杂,咱们必须借助于一个有效的DOM树比较算法。
设计一个diff算法有两个要点:
如何比较两个两棵DOM树 如何记录节点之间的差别
<1> 如何比较两个两棵DOM树
计算两棵树之间差别的常规算法复杂度为O(n3),一个文档的DOM结构有上百个节点是很正常的状况,这种复杂度没法应用于实际项目。针对前端的具体状况:咱们不多跨级别的修改DOM节点,一般是修改节点的属性、调整子节点的顺序、添加子节点等。所以,咱们只须要对同级别节点进行比较,避免了diff算法的复杂性。对同级别节点进行比较的经常使用方法是深度优先遍历:
function diff(oldTree, newTree) { //节点的遍历顺序 var index = 0; //在遍历过程当中记录节点的差别 var patches = {}; //深度优先遍历两棵树 dfsWalk(oldTree, newTree, index, patches); return patches; }
<2>如何记录节点之间的差别
因为咱们对DOM树采起的是同级比较,所以节点之间的差别能够归结为4种类型:
修改节点属性, 用PROPS表示 修改节点文本内容, 用TEXT表示 替换原有节点, 用REPLACE表示 调整子节点,包括移动、删除等,用REORDER表示
对于节点之间的差别,咱们能够很方便地使用上述四种方式进行记录,好比当旧节点被替换时:
{type:REPLACE,node:newNode}
而当旧节点的属性被修改时:
{type:PROPS,props: newProps}
在深度优先遍历的过程当中,每一个节点都有一个编号,若是对应的节点有变化,只须要把相应变化的类别记录下来便可。下面是具体实现:
function dfsWalk(oldNode, newNode, index, patches) { var currentPatch = []; if (newNode === null) { //依赖listdiff算法进行标记为删除 } else if (util.isString(oldNode) && util.isString(newNode)) { if (oldNode !== newNode) { //若是是文本节点则直接替换文本 currentPatch.push({ type: patch.TEXT, content: newNode }); } } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { //节点类型相同 //比较节点的属性是否相同 var propsPatches = diffProps(oldNode, newNode); if (propsPatches) { currentPatch.push({ type: patch.PROPS, props: propsPatches }); } //比较子节点是否相同 diffChildren(oldNode.children, newNode.children, index, patches, currentPatch); } else { //节点的类型不一样,直接替换 currentPatch.push({ type: patch.REPLACE, node: newNode }); } if (currentPatch.length) { patches[index] = currentPatch; } }
好比对上文图中的两颗虚拟DOM树,能够用以下数据结构记录它们之间的变化:
var patches = { 1:{type:REPLACE,node:newNode}, //h1节点变成h5 5:{type:REORDER,moves:changObj} //ul新增了子节点li }
经过虚拟DOM计算出两颗真实DOM树之间的差别后,咱们就能够修改真实的DOM结构了。上文深度优先遍历过程产生了用于记录两棵树之间差别的数据结构patches, 经过使用patches咱们能够方便对真实DOM作最小化的修改。
//将差别应用到真实DOM function applyPatches(node, currentPatches) { util.each(currentPatches, function(currentPatch) { switch (currentPatch.type) { //当修改类型为REPLACE时 case REPLACE: var newNode = (typeof currentPatch.node === 'String') ? document.createTextNode(currentPatch.node) : currentPatch.node.render(); node.parentNode.replaceChild(newNode, node); break; //当修改类型为REORDER时 case REORDER: reoderChildren(node, currentPatch.moves); break; //当修改类型为PROPS时 case PROPS: setProps(node, currentPatch.props); break; //当修改类型为TEXT时 case TEXT: if (node.textContent) { node.textContent = currentPatch.content; } else { node.nodeValue = currentPatch.content; } break; default: throw new Error('Unknow patch type ' + currentPatch.type); } }); }
原文来自于:http://blog.csdn.net/yczz/article/details/51292169