渲染真实DOM的开销是很大的,对DOM的操作会引起整个dom树的重绘和重排。因此,vue采用虚拟dom来对节点进行更新。比如当某个div的属性发生变化时,这时候只需要比较变化前的oldVnode和变换后的Vnode,删除多余属性,更新属性或添加新属性,而不需要删除整个dom元素。
diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁
virtual DOM 是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构。
真实DOMvirual DOM<div> <p>123</p></div>var Vnode = {tag:'div', data:{} children: [{tag: 'p', text: '123'}]}diff比较新旧节点的时候,只会在同层级进行,不会跨层级比较。
如图所示,即是数据变化前后,新旧节点(虚拟dom)的比较流程。
function patch(oldVnode,vnode) ..... // some code if(sameVnode(oldVnode,vnode)){ patchVnode(oldVnode,vnode) }else{ // 新旧节点不一样时 const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点 let parentEle = api.parentNode(oEL) //父元素 createEle(vnode) //根据Vnode生成新元素 if(parentEle !== null){ api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) //插入新节点 api.removeChild(parentEle, oldVnode.el) //移除旧节点 } .... // some code return vnode }整个patch过程,先检查新旧节点是否相似,
如果不相似,则根据vnode生成新dom节点插入到父元素里,并删除旧的节点如果相似,则先对data比较,包括class、style、event、props、attrs等,有不同就调用对应的update函数,然后对子节点children进行比较,children的比较用到了diff算法 function sameVnode(a,b){ return (a.key ==== b.key && ( (a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a,b)) || (isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error)) ) }简单来说,sameVnode就是比较是否有相同的key和tag。
简化后的vnode大致包含以下属性:
{ tag:'div', data:{}, //属性数据, 包括class style event props attrs等 children: [], //子节点数组 text: undefined, //文本 elem: undefined, // 真实dom key: undefined //节点标识 }先对data比较,包括class、style、event、props、attrs等以updateAttrs为例
function updateAttrs(oldVnode,vnode){ let key,cur, old const elm = vnode.elm const oldAttrs = oldVnode.data.attrs ||{} const attrs = vnode.data.attrs || {} for(key in attrs){ cur = attrs[key] //vnode中的attrs old = oldAttrs[key] if(old !== cur) { old这个属性与cur这个属性不一致了,则更新/添加 elm.setAttribute(key,cur) } } //删除节点中不存在的属性 for(key in oldAttrs){ if(!(key in attrs)){ //旧属性中存在,而新属性中不存在 elm.removeAttribute(key) } } }然后,当oldCh 和 ch(newCh)都存在时,比较子节点chidren,此过程就是diff
在patch过程中,当oldVnode和vnode均有children子节点时,这时候就会用到diff算法来进行比较。 以下图为例,
比较的方法流程:
第一步 头头比较,如果相似,旧头心头均后移,真实dom不变,进入下一次循环,如不相似,进入第二步第二步 尾尾比较,如果相似,旧尾新尾均前移,真是dom不变,进入下一次循环,如不相似,进入第三步第三步 头尾比较, 如果相似,将旧头插入到旧尾最后的位置。旧头后移,新尾前移。如不相似,进入下一次循环第四步 尾头比较,如果相似,将旧尾插入到旧头前面的位置。旧尾前移,新头后移。如不相似进入第五步第五步 若节点有key且再旧子节点数组中找到sameVnode(tag和key都一致),则将其dom移动到当前真实dom序列的头部,新头指针后移; 否则,vnode对应的dom(vnode[newStartIdx],elm)插入到当前真实dom序列的头部,新头指针后移以上这个循环结束的标志是
新的节点数组(newCh)被遍历完了(newStartIdx > newEndIdx)。则把多余的旧dom都删掉旧的节点数组(oldCh)被遍历完了(oldStartIdx > oldEndIdx)。则把多余的新dom都添加 //diff算法源码 function updateChildren(parentElm, oldCh, newCh,insertedVnodeQueue){ let oldStartIdx = 0; let newStartIdx = 0; let oldEndIdx = oldCh.length -1; let newEndIdx = newCh.length -1; let oldStartVnode = oldCh[0]; let oldEndVnode = oldCh[oldEndIdx]; let newStartVnode = newCh[0]; let newEndVnode = newCh[newEndIdx]; while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){ if(isUndef(oldStartVnode)){ oldStartVnode = oldCh[++oldStartIdx] //这个节点被移走了 }else if(isUndef(oldEndVnode)){ oldEndVnode = oldCh[--oldEndIdx] } else if(sameVnode(oldStartVnode, newStartVnode)){ //头头相似 patchVnode(oldStartVnode,newStartVnode) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if(sameVnode(oldEndVnode,newEndVnode)){ // 尾尾相似 pathVnode(oldEndVnode,newEndVnode) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if(sameVnode(oldStartVnode,newEndVnode)){ //头尾相似 Vnode moved right patchVnode(oldStartVnode,newEndVnode) api.insertBefore(parentElm,oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)) //把oldStartVnode插入到oldEndVnode后面 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if(sameVnode(oldEndVnode, newStartVnode)){ //尾头相等 Vnode moved left patchVnode(oldEndVnode,newStartVnode) api.inserBefore(parentElm,oldEndVnode.elm,api.nextSibling(oldStartVnode.elm)) // 把oldEndVnode插入到oldStartVnode的前面 } else { // 根据旧子节点的key,生成map映射 if(isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) //在旧子节点数组中,找到和newStartVnode相似节点的下标 idxInOld = oldKeyToIdx[newStartVnode.key] if(isUndef(idxInOld)){ api.insertBefore(parentElm,createElm(newStartVnode),oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] }else { elmToMove = oldCh[idxInOld] patchVnode(elmToMove,newStartVnode) oldCh[idxInOld] = undefined api.insertBefore(parentElm,elmToMove.elm, OldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } }注: isUndef函数 都说添加了 :key可以优化v-for的性能,到底是怎么回事呢? 因为v-for大部分情况下生成的都是相同的tag标签,如果没有key标识,那么相当于每次头头比较都能成功。如果你往v-for绑定的数组头部push数据,那么整个dom将全部刷新一遍。 有了key, 其实就是多了一项匹配查找。
参考:Vue源码解析:虚拟dom比较原理