underbody对应于oldVnodeis{el:div//对真实节点的引用,在本例中为document.querySelector('#id.classA')tagName:'DIV',//节点的标签sel:'div#v.classA'//节点的选择Data:null,//存储节点属性的对象,对应于el[prop]节点的属性,比如onclick,stylechildren:[],//一个存放子节点的数组,每个子节点也是一个vnode结构体text:null,//如果是文本节点,则对应文本节点的textContent,否则为null}需要注意的是,el属性指的是这个虚拟dom对应的真实dom,patch的vnode参数的el最初为null,因为它仍然补丁之前存在没有对应的真实dom。来到patch的第一部分,if(sameVnode(oldVnode,vnode)){patchVnode(oldVnode,vnode)}sameVnode函数是看两个节点是否值得比较,代码很简单:functionsameVnode(oldVnode),vnode){returnvnode.key===oldVnode.key&&vnode.sel===oldVnode.sel}两个vnode的key和sel相同比较,比如p和span,div.classA和div.classB被认为是不同的结构而不是比较它们。如果值得比较,就会执行patchVnode(oldVnode,vnode),后面会详细介绍patchVnode函数。当节点不值得比较时,在else中输入else{constoEl=oldVnode.elletparentEle=api.parentNode(oEl)createEle(vnode)if(parentEle!==null){api.insertBefore(parentEle,vnode.el,api.nextSibling(oEl))api.removeChild(parentEle,oldVnode.el)oldVnode=null}}过程如下:获取oldvnode.el的父节点,parentEle才是真正的domcreateEle(vnode)会创建它的vnode的真实dom,以便vnode.el=realdomparentEle插入新的dom并删除旧的dom。当不值得比较时,新节点直接替换旧节点。Finallyreturnvnodepatch最终会返回vnode。vnode和打补丁之前有什么区别?没错,就是vnode.el。唯一的变化是之前的vnode.el=null,现在指向对应的真实dom。varoldVnode=patch(oldVnode,vnode)至此,一个patch过程就完成了。patchVnode当两个节点值得比较时,patchVnode函数将被调用=vnode)returnif(oldVnode.text!==null&&vnode.text!==null&&oldVnode.text!==vnode.text){api.setTextContent(el,vnode.text)}else{updateEle(el,vnode,oldVnode)if(oldCh&&ch&&oldCh!==ch){updateChildren(el,oldCh,ch)}elseif(ch){createEle(vnode)//创建el的孩子dom}elseif(oldCh){api.removeChildren(el)}}}constel=vnode.el=oldVnode.el这是很重要的一步,让vnode.el引用当前真实的dom,修改el时,vnode.el会同步变化。节点比较if(oldVnode===vnode)有5种情况,它们的引用是一致的,可以认为没有变化。if(oldVnode.text!==null&&vnode.text!==null&&oldVnode.text!==vnode.text),需要修改文本节点的比较,会调用Node.textContent=vnode.text.if(oldCh&&ch&&oldCh!==ch),两个节点都有子节点,而且是不同的,所以我们会调用updateChildren函数来比较子节点,这个是diff的核心,后面会讲到。elseif(ch),只有新节点有子节点,调用createEle(vnode),vnode.el已经引用了旧dom节点,createEle函数会将子节点添加到旧dom节点。elseif(oldCh),新节点没有子节点,老节点有子节点,直接删除老节点。updateChildrenupdateChildren(parentElm,oldCh,newCh){letoldStartIdx=0,newStartIdx=0letoldEndIdx=oldCh.length-1letoldStartVnode=oldCh[0]letoldEndVnode=oldCh[oldEndIdx]letnewEndIdx=newCh.length-1letnewStartVnode=newCh[0]letnewEndVnode=newCh[newEndIdx]letoldKeyToIdxletidxInOldletelmToMoveletbeforewhile(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){if(oldStartVnode==null){//对vnode.key的比较,会把oldVnode=nulloldStartVnode=oldCh[++oldStartIdx]}elseif(oldEndVnode==null){oldEndVnode=oldCh[--oldEndIdx]}elseif(newStartVnode==null){newStartVnode=newCh[++newStartIdx]}elseif(newEndVnode==null){newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldStartVnode,newStartVnode)){patchVnode(oldStartVnode,newStartVnode)oldStartVnode=oldCh[++oldStartIdx]newStartVnode=newCh[++newStartIdx]}elseif(sameVnode(oldEndVnode,newEndVnode)){patchVnode(oldEndVnode,newEndVnode)oldEndVnode=oldCh[--oldEndIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldStartVnode,newEndVnode)){patchVnode(oldStartVnode,newEndVnode)api.insertBefore(parentElm,oldStartVnode.el,api.nextSibling(oldEndVnode.el))oldStartVnode=oldCh[++oldStartIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldEndVnode,newStartVnode)){patchVnode(oldEndVnode,newStartVnode)api.insertBefore(parentElm,oldEndVnode.el,oldStartVnode.el)oldEndVnode=oldCh[--oldEndIdx]newStartVnode=newCh[++newStartIdx]}其他{//使用密钥时的比较if(oldKeyToIdx===undefined){oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx)//有密钥生成索引表}idxInOld=oldKeyToIdx[newStartVnode.key]if(!idxInOld){api.insertBefore(parentElm,createEle(newStartVnode).el,oldStartVnode.el)newStartVnode=newCh[++newStartIdx]}else{elmToMove=oldCh[idxInOld]if(elmToMove.sel!==newStartVnode.sel){api.insertBefore(parentElm,createEle(newStartVnode).el,oldStartVnode.el)}else{patchVnode(elmToMove,newStartVnode)oldCh[idxInOld]=nullapi.insertBefore(parentElm,elmToMove.el,oldStartVnode.el)}newStartVnode=newCh[++newS]tartIdx]}}}if(oldStartIdx>oldEndIdx){before=newCh[newEndIdx+1]==null?null:newCh[newEndIdx+1].eladdVnodes(parentElm,before,newCh,newStartIdx,newEndIdx)}elseif(newStartIdx>newEndIdx){removeVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx)}}代码非常密集。为了形象的描述这个过程,可以看这张图。其过程可以概括为:oldCh和newCh各有两个头尾变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果四个比较都不匹配,如果设置了key,则使用key进行比较。在比较过程中,变量会向中间倾斜。一旦StartIdx>EndIdx表示至少遍历了oldCh和newCh之一,就结束Compare。具体diff分析设置key和不设置key的区别:不设置key,newCh和oldCh只会比较头尾端。设置key后,除了进行头尾比较外,key生成的对象会在oldKeyToIdx中找到匹配的节点,所以为节点设置key可以更高效的利用dom。diff遍历过程中,只要对dom进行操作,就会调用api.insertBefore,api.insertBefore只是对原生insertBefore的简单封装。有两个对比,一个有vnode.key,一个没有。但是这两个比较在真实的DOM上以相同的方式运行。对于sameVnode(oldStartVnode,newStartVnode)和sameVnode(oldEndVnode,newEndVnode)为真的情况,不需要移动dom。总结遍历过程,dom操作分为三种:当oldStartVnode和newEndVnode值得比较时,说明oldStartVnode.el已经落后于oldEndVnode.el。图中假设startIdx遍历到1,在oldEndVnode和newStartVnode值得比较的时候,oldEndVnode.el跑到了oldStartVnode.el的前面,准确的说应该是oldEndVnode.el需要移动到oldStartVnode.el前面。newCh中的节点不在oldCh中,将新节点插入oldStartVnode.el前面。最后有两种情况:oldStartIdx>oldEndIdx,可以认为oldCh已经完成先遍历,当然也有可能此时newCh刚遍历完,都归到这一类,此时newStartIdx和newEndIdx之间的vnode是新添加的,调用addVnodes将它们都插入到后面before.before经常为null,addVnodes调用insertBefore操作dom节点,看下insertBefore的文档:parentElement.insertBefore(newElement,referenceElement)如果referenceElement为null,newElement会插入到子节点的末尾.如果newElement已经在DOM树中,newElement将首先从DOM树中移除。所以before为null,newElement会插入到子节点的末尾。newStartIdx>newEndIdx,可以认为newCh先遍历过了。此时oldStartIdx和oldEndIdx之间的vnode在新的子节点中已经不存在了。调用removeVnodes将它们从dom中删除。下面是一个例子,绘制了一个完整的diff过程,dom变化的每一步都用不同颜色的线标出。A、b、c、d、e被假定为4个不同的元素。当我们不设置key的时候,b并没有被复用,而是直接新建了一个,把旧的删掉。当我们向4个元素添加唯一键时,b会被重用。在这个例子中,如果我们使用手动优化,只需要3步就可以实现。综上所述,尽量不要跨层修改dom设置key,最大限度的利用节点。不要盲目相信diff的效率。您可以在必要时手动优化它。