# Virtual Dom
## "昂贵"的DOM
我们可以做个试验。打印出一个空元素的第一层属性,可以看到标准让元素实现的东西太多了。如果每次都重新生成新的元素,对性能是巨大的浪费。
![](http://p878i6l4k.bkt.clouddn.com/%22%E6%98%82%E8%B4%B5%22%E7%9A%84DOM.png)
#### Virtual Dom就是解决这个问题的一个思路,到底什么是Virtual Dom呢?
>通俗易懂的来说就是用一个简单的JS对象去代替复杂的dom对象。
举个简单的例子,我们在body里插入一个class为a的div。
```js
var mydiv = document.createElement('div');
mydiv.className = 'a';
document.body.appendChild(mydiv);
```
对于这个div我们可以用一个简单的对象VNode代表它,它存储了对应dom的一些重要参数,在改变dom之前,会先比较相应虚拟dom的数据,如果需要改变,才会将改变应用到真实dom上。
```js
//伪代码
var VNode = {
tagName: 'DIV',
className: 'a'
};
mydiv.className = 'b'; // 改变class属性
var oldVNode = VNode
var newVNode = {
tagName: 'DIV',
className: 'b'
}
if(oldVNode.tagName !== newVNode.tagName || oldVNode.className !== newVNode.className){
patch(mydiv)
}
```
#### 读到这里就会产生一个疑问,为什么不直接修改dom而需要加一层Vrtual Dom呢?
> 很多时候手工优化dom确实会比virtual dom效率高,对于比较简单的dom结构用手工优化没有问题,但当页面结构很庞大,结构很复杂时,手工优化会花去大量时间,而且可维护性也不高,不能保证每个人都有手工优化的能力。
> 至此,Vrtual Dom的解决方案应运而生,Vrtual Dom很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。
## Vrtual Dom更新过程的实现
> 网上有太多的人讲Vrtual Dom的实现过程。特别是其中的diff算法。但是,对于新手来说,这些文章会让你似懂非懂。
> 我觉得最主要的原因是没有对`key`这个属性进行很好的解释。
首先介绍两个相关概念
### diff 算法
VDom因为是纯粹的JS对象,所以操作它会很高效,但是VDom的变更最终会转换成DOM操作,为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要。
先看整体视图,整个diff分两部分:
![](http://p878i6l4k.bkt.clouddn.com/diff%E7%AE%97%E6%B3%95.png)
#### (一)、优先处理特殊场景
(1)、头部的同类型节点、尾部的同类型节点
>这类节点更新前后位置没有发生变化,所以不用移动它们对应的DOM
(2)、头尾/尾头的同类型节点
>这类节点位置很明确,不需要再花心思查找,直接移动DOM就好
处理了这些场景之后,一方面一些不需要做移动的DOM得到快速处理,另一方面待处理节点变少,缩小了后续操作的处理范围,性能也得到提升
#### (二)、“原地复用”
原地复用”是指Vue会尽可能复用DOM,尽可能不发生DOM的移动。
> Vue在判断更新前后指针是否指向同一个节点,其实不要求它们真实引用同一个DOM节点,实际上它仅判断指向的是否是同类节点(比如2个不同的div,在DOM上它们是不一样的,但是它们属于同类节点),如果是同类节点,那么Vue会直接复用DOM,这样的好处是不需要移动DOM。
#### (三)、
### key属性在列表渲染中的作用
**官方解释:**
> 当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。这个类似 Vue 1.x 的 track-by="$index" 。
> 为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。理想的 key 值是每项都有的且唯一的 id。这个特殊的属性相当于 Vue 1.x 的 track-by ,但它的工作方式类似于一个属性,所以你需要用 v-bind 来绑定动态值
我对官方的提炼:
* 节点识别
* “原地复用”策略
* 默认类似 track-by="$index"
#### 下面将从列表元素的渲染更新过程来介绍Vrtual Dom更新过程
![](http://p878i6l4k.bkt.clouddn.com/%E8%99%9A%E6%8B%9FDOM-1.png)
Vue分为在oldVDom树设置oldStart和oldEnd指针,为newVDom树设置newStart和newEnd指针。如下图所示:
![](http://p878i6l4k.bkt.clouddn.com/%E8%99%9A%E6%8B%9FDOM-2.png)
新、旧虚拟DOM树比较的的过程就是调用patch函数,就像打补丁一样修改真实dom。同时,修改相应指针的指向,使其循环遍历所有同层节点。
```
// 伪代码
function patch() {
if (oldVNode.key === newVNode.key) {
// 如果两VNode节点的身份标识符相同(key)
// 则对oldVNode进行更新操作
updateChildren(oldVNode, newVNode);
} else if ( vnode.el === oldVnode.el) {
// 如果两VNode节点属于同类型
// 则对oldVNode进行复用操作
patchVnode(oldVNode, newVNode)
} else if (!oldVNode.el && newVNode.el) {
// 如果oldVNode中不存在,newVNode中存在
// 则对oldVNode执行插入操作
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
let newDom = createEle(vnode)
api.insertBefore(parentEle, newDom, api.nextSibling(oEl))
} else if (oldVNode.el && !newVNode.el) {
// 如果oldVNode中存在,newVNode中不存在
// 则对oldVNode执行删除操作
let parentEle = api.parentNode(oEl)
api.removeChild(parentEle, oldVnode.el)
}
}
```
具体过程如下图所示
![](http://p878i6l4k.bkt.clouddn.com/%E8%99%9A%E6%8B%9FDOM-3.png)
最后patch过程为
![](http://p878i6l4k.bkt.clouddn.com/%E8%99%9A%E6%8B%9FDOM-4.png)
可以看到:
> 对A、B节点进行更新;
> 对C、D节点进行复用;
> 插入D节点。
显然,更新的性能损耗要小于复用。因此,这样的更新效率是比较低的。但是,这就是diff 算法的“原地复用”策略。
现在我们来看看当存在key属性是的patch过程
设置key和不设置key的区别:
> 不设key,oldVNode和newVNode只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象`oldKeyToIdx`中查找匹配的节点。
```js
oldKeyToIdx = createKeyToOldIdx(oldVNode, oldStart, oldEnd) // 有key生成index表
idxInOld = oldKeyToIdx[newStart.key]
```
> 所以为节点设置key可以更高效的利用dom。
![](http://p878i6l4k.bkt.clouddn.com/%E8%99%9A%E6%8B%9FDOM-5.png)
可以看到:
> 对A、B、C、D节点进行更新;
> 在正确的位置插入F节点;
通过虚拟DOM计算出两颗虚拟DOM树之间的差异后,我们就可以用尽可能小的代价修改真实的DOM树结构。在效率、可维护性之间达平衡。
至此,关于Vue的虚拟DOM的内容讲完了,想更加深入了解的可以直接看源码。我相信你看懂我这篇文章再看源码应该难度会小很多。