本文原连接:https://blog.csdn.net/qq_33050575/article/details/82885788css
http://www.javashuo.com/article/p-ybghuplm-k.htmlhtml
虚拟DOM解析及其在框架里的应用
浏览器是怎样解析HTML而且绘出整个页面的前端
上图为webkit引擎浏览器的处理流程,如上图大体分为4大步:vue
第一步,HTML解析器分析html,构建一颗DOM树;react
第二步,CSS解析器会分析外联的css文件和内联的一些样式,建立一个页面的样式表;jquery
第三步,将DOM树和样式表关联起来,建立一颗Render树。这一过程又被称为Attachment,每一个DOM节点上都有一个attach方法,会接收对应的样式表,返回一个render对象。这些render对象最终会结合成一个render tree;web
第四步,有了render tree后,浏览器就能够为render tree上的每一个节点在屏幕上分配一个精确的位置坐标,而后各个节点会调用自身的paint方法结合坐标信息,在浏览器中绘制中整个页面了。算法
回流(reflow)和重绘
回流和重绘都是浏览器自身的行为api
回流:当render tree中的一部分(或所有)由于元素的规模尺寸,布局,隐藏等改变而须要从新构建。这就称为回流(reflow)。每一个页面至少须要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并从新构造这部分渲染树,完成回流后,浏览器会从新绘制受影响的部分到屏幕中,该过程成为重绘。浏览器
重绘:当render tree中的一些元素须要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,好比background-color。则就叫称为重绘。
减小回流的办法(不是此次讨论重点):好比cssText、避免使用会强制reflush队列的属性等等
手动操做DOM带来的性能忧患
在使用原生的js api或者jquery等一些方法直接去操做dom的时候,可能会引发页面的reflow,而页面的回流所带来的代价是很是昂贵的。频繁的去操做dom,会引发页面的卡顿,影响用户的体验。
这里打印一个最简单的真实DOM节点里面的属性
虚拟DOM就是为了解决频繁操做DOM的性能问题创造出来的。
例如:
若是使用原生api去操做一个会致使回流的DOM操做10次,那么浏览器会每次都会从新走一次上面的全流程,包括一些没变化的位置计算。
而虚拟DOM不会当即去操做DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,通知浏览器去执行绘制工做,这样能够避免大量的无谓的计算量。
virtual dom 是什么
这是一段真实的DOM tree结构
<div id="container">
<p>Real DOM</p>
<ul>
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>
</div>
若是用js对象来模拟上述的DOM Tree
let virtualDomTree = CreateElement('div', { id: 'container' }, [
CreateElement('p', {}, ['Virtual DOM']),
CreateElement('ul', {}, [
CreateElement('li', { class: 'item' }, ['Item 1']),
CreateElement('li', { class: 'item' }, ['Item 2']),
CreateElement('li', { class: 'item' }, ['Item 3']),
]),
]);
let root = virtualDomTree.render(); //转换为一个真正的dom结构或者dom fragment
document.getElementById('virtualDom').appendChild(root);
这样的好处,避免了因直接去修改真实的dom而带来的性能隐患。能够先把页面的一些改动反应到这个虚拟dom对象上,等更新完后再一次统一去把变化同步到真实的dom中。
下面是CreateElement的实现方法
function CreateElement(tagName, props, children) {
if (!(this instanceof CreateElement)) {
return new CreateElement(tagName, props, children);
}
this.tagName = tagName;
this.props = props || {};
this.children = children || [];
this.key = props ? props.key : undefined;
}
render方法
CreateElement.prototype.render = function() {
let el = document.createElement(this.tagName);
let props = this.props;
for (let propName in props) {
setAttr(el, propName, props[propName]);
}
this.children.forEach((child) => {
let childEl = (child instanceof Element) ? child.render() : document.createTextNode(child);
el.appendChild(childEl);
});
return el;
};
直到如今,已经能够在页面中建立一个真实的DOM结构了。
diff算法
上面已经完成了虚拟DOM -> 真实DOM的一个转换工做,如今只须要把页面上全部的改动都更新到虚拟DOM上。这就是一个diff过程。
两棵树若是彻底比较时间复杂度是O(n^3),但参照《深刻浅出React和Redux》一书中的介绍,React的Diff算法的时间复杂度是O(n)。要实现这么低的时间复杂度,意味着只能平层地比较两棵树的节点,放弃了深度遍历。这样作,彷佛牺牲了必定的精确性来换取速度,但考虑到现实中前端页面一般也不会跨层级移动DOM元素,因此这样作是最优的。
只考虑相同等级diff,能够分为下面4中状况:
第一种。若是节点类型变了,好比下面的p标签变成了h3标签,则直接卸载旧节点装载新节点,这个过程称为REPLACE。
第二种状况。节点类型同样,仅仅是属性变化了,这一过程叫PROPS。好比
renderA: <ul>
renderB: <ul class: 'marginLeft10'>
=> [addAttribute class "marginLeft10"]
这一过程只会执行节点的更新操做,不会触发节点的卸载和装载操做。
第三种。只是文本变化了,TEXT过程。该过程只会替换文本。
第四种。节点发生了移动,增长,或者删除操做。该过程称为REOREDR。虚拟DOM Diff算法解析
若是在一些节点中间插入一个F节点,简单粗暴的作法是:卸载C,装载F,卸载D,装载C,卸载E,装载D,装载E。以下图:
这种方法显然是不高效的。
而若是给每一个节点惟一的标识(key),那么就能找到正确的位置去插入新的节点。
这也就是为何vue/react等框架,会要求咱们在写循环遍历结构的时候要写key值
在vue2.0中是如何使用虚拟dom来绑定和渲染模板的
监听数据的变化
vue内部是经过数据劫持的方式来作到数据绑定的,其中最核心的方法就是经过Object.defineProperty()的getter和setter来实现对数据劫持,达到监听数据变更的目的。
vue中虚拟DOM生成DOM的过程
DocumentFragment和vue异步更新队列
DocumentFragment是允许把一些DOM操做先应用到一个dom片断里,而后再将这个片断append到DOM树里,从而来减小页面的reflow次数。
var fragment = document.createDocumentFragment();
//add DOM to fragment
for(var i = 0; i < 10; i++) {
var spanNode = document.createElement("span");
spanNode.innerHTML = "number:" + i;
fragment.appendChild(spanNode);
}
//add this DOM to body
document.body.appendChild(spanNode);
vue里面的一些dom的一些变化,都是在DocumentFragment容器中去操做的,最后将这个更新片断append到el的根dom中。
走到这里,你会发现还有一个问题。就算vue使用了虚拟dom,将一些改动先同步到虚拟对象上,而后去改动真实DOM。这其中,去改动真实DOM仍是使用了原生的api去操做DOM,仍是会不可避免的去reflow整个页面,若是不能把这些更新操做打包起来集中去更新真实DOM,那其实彻底散失了虚拟DOM的做用性,反而变得更加冗余。
这时候框架的价值就体现出来了,vue中,若是在同一次事件循环中若是观察到有多个数据变化,vue会开启一个异步更新队列,并缓冲在同一事件循环中发生的全部数据改变。而后在下一个的事件循环‘tick’中,vue刷新队列并执行实际工做。这样就能够批量的去更新屡次数据变化到虚拟dom对象中,diff差别,同步到页面中的真实dom里。