Vue在2.0版本引入了虚拟DOM。其虚拟DOM算法是基于snabbdom算法所作的修改。参看https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js注释部分。要想了解Vue,必须了解虚拟DOM,本篇文章主要介绍了什么是虚拟DOM,为何用虚拟DOM以及其具体实现。html
用JavaScript模拟DOM树造成虚拟DOM树,以下面的html结构前端
<ul style="color:#000"> <li>苹果</li> <li>香蕉</li> <li>橙子</li> </ul>
可使用以下JS表示vue
{ sel: 'ul', data: { style: {color: '#000'}}, // 节点属性及绑定事件等 children: [ // 子节点 {sel: 'li', text: '苹果'}, {sel: 'li', text: '香蕉'}, {sel: 'li', text: '橙子'} ] }
由于对DOM的直接操做是很是慢并且低效的。浏览器的渲染流程包括解析html以构建dom树->构建render树->布局render树->绘制render树,而每一次DOM改变从构建render树到布局到渲染都要重来。参考文档node
而虚拟DOM的优点就是:1.开发者再也不关心DOM而只关心数据,提高开发效率。2.保证最小化的DOM操做,使执行效率获得提高。react
虚拟DOM的优点并不在于它操做DOM比较快,而是可以经过虚拟DOM的比较,最小化真实DOM操做, 参考文档
实现虚拟DOM包含如下三个步骤git
虚拟DOM对象包含如下属性:github
参考https://github.com/snabbdom/snabbdom/blob/master/src/tovnode.ts算法
给定任意两棵树,找到最少的转换步骤。可是标准的的Diff算法复杂度须要O(n^3). segmentfault
这显然没法知足性能的要求,考虑到前端操做的状况--咱们不多跨级别的修改节点,一般是修改节点的属性、调整子节点的顺序、添加子节点等。当比较虚拟DOM树的时候,若是发现节点已经不存在,则该节点及其子节点会被彻底删除掉,不会用于进一步的比较。这样只须要对树进行一次遍历,便能完成整个DOM树的比较。api
虚拟DOM在比较时只比较同层次节点,其复杂度下降到了O(n). 并且比较时只比较其key和sel是否相同,相同即为相同节点。
function sameVnode(vnode1: VNode, vnode2: VNode): boolean { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }
例子:下图节点从左图变为右图
虚拟DOM的作法是
A.destroy(); A = new A(); A.append(new B()); A.append(new C()); D.append(A);
而不是
A.parent.remove(A); D.append(A);
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { ... const elm = vnode.elm = (oldVnode.elm as Node); let oldCh = oldVnode.children; let ch = vnode.children; if (oldVnode === vnode) return; // 都是undefined ... if (isUndef(vnode.text)) { // 新节点不是textNode if (isDef(oldCh) && isDef(ch)) { // 子节点都存在,updateChildren对子节点进行diff if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue); } else if (isDef(ch)) { // 旧节点没有子节点,且新节点有子节点。将新节点的子节点添加进来 if (isDef(oldVnode.text)) api.setTextContent(elm, ''); addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { // 新节点没有子节点,且旧节点有子节点。 删除旧节点的子节点 removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1); } else if (isDef(oldVnode.text)) { // 新旧节点都没有子节点。更新text api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { // 新节点是textNode且新旧不一致 api.setTextContent(elm, vnode.text as string); } ... }
若是两个元素相同(key和sel),则判断其children,过程当中维护四个变量
例以下图中children由ABCDEF -> ADGCEF,其中假设其sel相同且都设置有key,A的key为A,B的key为B,依次类推
循环判断以下:
参看源码https://github.com/snabbdom/snabbdom/blob/master/src/snabbdom.ts#L179
为何维护四个变量?有什么优点?两个变量是否能够?此处留个疑问。
oldStart === newStart,则执行上面3.3. 且oldStartIdx++, newStartIdx++.
oldEnd === newEnd,则执行上面3.3. 且oldEndIdx--, newEndIdx--.
同上,oldEnd === newEnd,则执行上面3.3. 且oldEndIdx--, newEndIdx--.
oldEnd === newStart,将oldEnd插入到oldStart以前,并执行上面3.3. 且oldEndIdx--, newStartIdx++.
首尾元素均不相同!判断newStart在旧元素中是否存在,存在则移动,不然将新元素插入
oldKeyToIdx = [B, C] // 从oldStartIdx到oldEndIdx的全部元素 G in [B, C] ? NO!
将newStart插入到oldStart以前,并执行上面3.3.且newStartIdx++.
同上。H in [B, C] ? NO! 将newStart插入到oldStart以前,并执行3.3.且newStartIdx++.
新节点遍历完成。跳出循环,依次删除B和C。结束
至此,循环遍历结束。如今回答上面的问题,为何维护四个变量?有什么优点?两个变量是否能够?两个变量固然是能够的,四个变量的优点在于:四个变量能够更好的应对插入的场景。例如: