🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 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的内容讲完了,想更加深入了解的可以直接看源码。我相信你看懂我这篇文章再看源码应该难度会小很多。