当前位置: 首页 > Web前端 > vue.js

vuepatch源码分析(diff)

时间:2023-03-31 17:35:31 vue.js

patch过程组件页面渲染,以render返回的新vnode(新节点)和组件实例保存的vnode(旧节点)为参数,调用patch方法更新DOM。在判断两个节点是否相同的过程中,需要判断节点是否相同。相同的节点需要满足以下条件:相同的key,相同的标签类型,相同的评论节点ID,相同的评论节点,或者没有评论节点,数据的值状态相同,或者两者都有avalue,orhavenovaluefunctionsameVnode(a,b){//判断两个VNode节点是否为同一个节点return(a.key===b.key&&//key是否相同(a.tag===b.tag&&//tag相同a.isComment===b.isComment&&//注释节点标识相同isDef(a.data)===isDef(b.data)&&//数据值状态是否相同sameInputType(a,b)//输入类型相同))}patch方法补丁判断过程如下:a)如果新节点为空,旧节点存在(组件销毁时),调用旧节点的destroy生命周期函数b)如果旧节点为空,则基于新节点创建DOMc)其他(如果新旧节点都存在)a)旧节点不是DOM(组件节点)),新旧节点相同执行patchVnodeb)旧节点是一个DOM元素或者两个节点不一样创建一个新节点DOM,销毁旧节点和DOM。functionpatch(oldVnode,vnode,hydrating,removeOnly){if(isUndef(vnode)){if(isDef(oldVnode)){invokeDestroyHook(oldVnode);}return}...if(isUndef(oldVnode)){isInitialPatch=true;//组件初始加载createElm(vnode,insertedVnodeQueue);}else{varisRealElement=isDef(oldVnode.nodeType);if(!isRealElement&&sameVnode(oldVnode,vnode)){patchVnode(oldVnode,vnode,insertedVnodeQueue,null,null,removeOnly);}else{...varoldElm=oldVnode.elm;varparentElm=nodeOps.parentNode(oldElm);//获取父元素//创建新节点createElm(vnode,insertedVnodeQueue,oldElm._leaveCb?null:parentElm,nodeOps.nextSibling(oldElm)//获取下一个兄弟元素);if(isDef(parentElm)){removeVnodes(parentElm,[oldVnode],0,0);//销毁旧节点和DOM元素}elseif(isDef(oldVnode.tag)){invokeDestroyHook(oldV节点);}}}invokeInsertHook(vnode,insertedVnodeQueue,isInitialPatch);returnvnode.elm}}patchVnode方法当两个节点相同时,执行patchVnode方法,在处理各种情况之前将旧节点的elm属性值赋值给新节点的elm属性,保持elm一致。具体过程如下:a)如果新旧节点完全相同(指同一个oldVnode===vnode),不处理直接返回b)如果新节点不是文本节点a)有子节点,且新旧节点的子节点数组引用不同(oldCh!==ch)updateChildrenb)新节点有子节点,旧节点没有1)检查子节点(key)2)如果旧节点是文本节点,先清除文本3)创建子节点DOM元素c)旧节点有子节点,新节点不移除子节点和DOMd)旧节点是文本节点并清除文本c)如果新节点是文本节点,与旧节点的文本不同,则直接替换文本内容。d)其他(新节点为文本节点,与旧节点相同)不处理...if(isUndef(vnode.text)){if(isDef(oldCh)&&isDef(ch)){if(oldCh!==ch){updateChildren(elm,oldCh,ch,insertedVnodeQueue,removeOnly);}}elseif(isDef(ch)){if(process.env.NODE_ENV!=='production'){checkDuplicateKeys(ch);}if(isDef(oldVnode.text)){nodeOps.setTextContent(elm,'');}addVnodes(elm,null,ch,0,ch.length-1,insertedVnodeQueue);}elseif(isDef(oldCh)){removeVnodes(elm,oldCh,0,oldCh.length-1);}elseif(isDef(oldVnode.text)){nodeOps.setTextContent(elm,'');}}elseif(oldVnode.text!==vnode.text){nodeOps.setTextContent(elm,vnode.text);}...}updateChildren方法updateChildren方法处理新旧节点相同的Child节点。该方法定义了如下变量(updateChildren的节点代表子节点):varoldStartIdx=0;//表示当前正在处理的旧起始节点序号varnewStartIdx=0;//表示当前正在处理的新起始节点序号varoldEndIdx=oldCh.length-1;//表示当前正在处理的旧结束节点的序号varoldStartVnode=oldCh[0];//表示当前正在处理的旧起始节点varoldEndVnode=oldCh[oldEndIdx];//表示当前正在处理的旧结束节点varnewEndIdx=newCh.length-1;//表示当前正在处理的新结束节点序号varnewStartVnode=newCh[0];//表示当前新开始节点beingprocessedvarnewEndVnode=newCh[newEndIdx];//表示当前正在处理的新端节点varoldKeyToIdx,//未处理的旧节点键值映射idxInOld,//与新节点键值相同的旧节点序号vnodeToMove,//ThenewnodekeyTheoldnodewithsamevaluerefElm;//指向当前正在处理的新结束节点的下一个节点(已处理)的DOM元素。根据新旧节点的比较结果,更新DOM元素。这个过程不会改变新旧节点的顺序。序号指向正在处理的节点,分别是新旧节点的起始节点和结束节点。比较过程以新的起始节点为主导,比较方向为从两侧向中间。首先比较新旧节点的起始节点和结束节点,然后找到相同的未处理的旧节点作为新的起始节点。当所有的旧节点都处理完了(旧的起始和结束序号重叠),此时可能还没有处理到新节点,则添加一个新的节点DOM元素。当处理完所有新节点时(新的开始和结束序号重叠),可能还有旧节点,删除旧节点DOM元素。具体过程如下:当新旧子节点的起始序号不大于结束序号时,执行以下过程:a)如果旧子节点两边都有未定义的节点,旧起始节点未定义,oldStartVnode=oldCh[++oldStartIdx]旧结束节点未定义,oldEndVnode=oldCh[--oldEndIdx]b)新旧子节点的起始节点相同(前后对比)patchVNode更新DOM内容oldStartVnode=oldCh[++oldStartIdx]newStartVnode=newCh[++newStartIdx]c)新旧子节点结束节点相同(前后比较)patchVNode更新DOM内容oldEndVnode=oldCh[--oldEndIdx]newEndVnode=newCh[--newEndIdx]d)旧的起始节点和新的结束节点相同(前后对比)patchVNode更新DOM内容替换旧的起始节点DOM被添加到旧端节点DOM的前面oldStartVnode=oldCh[++oldStartIdx]newEndVnode=newCh[--newEndIdx]e)旧的结束节点和新的开始节点相同(前后对比)patchVNode更新DOM内容添加旧的结束节点DOMOldEndVnode=oldCh[--oldEndIdx]newStartVnode=newCh[++newStartIdx]f)Others(将未处理的老节点key值缓存在老起始节点DOM前面,判断老节点中是否有与新起始节点相同的key值)Node)a)未处理的旧节点中不存在与新起始节点相同的节点。创建一个新的节点DOM,并将其添加到旧起始节点DOM的前面与新起始节点相同的旧节点patchVode将相同的旧节点DOM添加到旧起始节点DOM的前面将相同的旧节点设置为undefinedoldCh[idxInOld]=undefinednewStartVnode=newCh[++newStartIdx]b)的key相同,只是标签类型不同的节点创建一个新的节点DOM,并将其添加到旧起始节点DOM的前面newStartVnode=newCh[++newStartIdx]循环结束a)如果旧节点已经遍历过(oldStartIdx>oldEndIdx),则将剩余未处理的新节点DOM添加到之前的新结束节点DOM前面(从新开始节点到新节点DOM)结束节点,全部未处理)b)如果已经遍历过新节点(newStartIdx>newEndIdx),移除旧的开始和结束节点以及它们之间的节点的DOM(从旧的开始节点到旧的结束节点,可能有待处理节点,但已处理为undefined)functionupdateChildren(parentElm,oldCh,newCh,insertedVnodeQueue,removeOnly){varoldStartIdx=0;//表示当前正在处理的旧起始节点序号varnewStartIdx=0;//表示新的起始节点的序号varoldEndIdx=oldCh.length-1;//表示当前正在处理的旧的结束节点的序号varoldStartVnode=oldCh[0];//表示旧的起始节点当前正在处理varoldEndVnode=oldCh[oldEndidx];//表示当前正在处理的旧端节点varnewEndIdx=newCh.length-1;//表示当前正在处理的新端节点的序号varnewStartVnode=newCh[0];//表示新的开始当前正在处理的节点varnewEndVnode=newCh[newEndIdx];//表示当前正在处理的新结束节点varoldKeyToIdx,idxInOld,vnodeToMove,refElm;...while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){if(isUndef(oldStartVnode)){oldStartVnode=oldCh[++oldStartIdx];//Vnode已向左移动}elseif(isUndef(oldEndVnode)){oldEndVnode=oldCh[--oldEndIdx];}elseif(sameVnode(oldStartVnode,newStartVnode)){patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue,newCh,newStartIdx);oldStartVnode=oldCh[++oldStartIdx];newStartVnode=newCh[++newStartIdx];}elseif(sameVnode(oldEndVnode,newEndVnode)){patchVnode(oldEndVnode,newEndVnode,insertedVnodeQueue,newCh,newEndIdx);oldEndVnode=oldCh[--oldEndIdx];newEndVnode=newCh[--newEndIdx];}elseif(sameVnode(oldStartVnode,newEndVnode)){//Vnode向右移动patchVnode(oldStartVnode,newEndVnode,insertedVnodeQueue,newCh,newEndIdx);canMove&&nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm));oldStartVnode=oldCh[++oldStartIdx];newEndVnode=newCh[--newEndIdx];}elseif(sameVnode(oldEndVnode,newStartVnode)){//Vnode向左移动patchVnode(oldEndVnode,newStartVnode,insertedVnodeQueue,newCh,newStartIdx);canMove&&nodeOps.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm);oldEndVnode=oldCh[--oldEndIdx];newStartVnode=newCh[++newStartIdx];}else{if(isUndef(oldKeyToIdx)){oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx);}//保存尚未处理的旧节键值idxInOld=isDef(newStartVnode.key)?oldKeyToIdx[newStartVnode.key]:findIdxInOld(newStartVnode,oldCh,oldStartIdx,oldEndIdx);if(isUndef(idxInOld)){//新元素createElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx);}else{vnodeToMove=oldCh[idxInOld];if(sameVnode(vnodeToMove,newStartVnode)){patchVnode(vnodeToMove,newStartVnode,insertedVnodeQueue,newCh,newStartIdx);oldCh[idxInOld]=u未定义;canMove&&nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm);}else{//相同的键但不同的元素。视为新元素createElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx);}}newStartVnode=newCh[++newStartIdx];}}如果(oldStartIdx>oldEndIdx){refElm=isUndef(newCh[newEndIdx+1])?空:newCh[newEndIdx+1].elm;addVnodes(parentElm,refElm,newCh,newStartIdx,newEndIdx,insertedVnodeQueue);}elseif(newStartIdx>newEndIdx){removeVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx);}}updateChildren示例:1.左边代表新老节点,start和end节点(也就是正在处理的节点)右边代表当前DOM。2.新节点的起始节点和结束节点与旧节点不同,在旧节点中没有找到与新起始节点(新节点f)相同的节点。于是创建节点f的DOM,并添加到老起始节点(老节点a)的DOM前面,然后在新起始节点的序号上加1,表示新节点f已经处理完毕,新的起始节点c当前正在处理中。3.新节点的起始节点和结束节点与旧节点不同,但在旧节点中找到与新起始节点(节点c)相同的节点。因此,将旧节点c的DOM添加到旧起始节点(旧节点a)的DOM前面,将旧节点c留空,然后在新起始节点的序号上加1,表示新节点c已经处理完毕,当前正在处理新的起始节点e。4.新的起始节点(新节点e)与旧的结束节点(旧节点e)相同。更新老节点e的DOM内容,将老节点e的DOM移动到老起始节点(老节点a)的DOM前面,老结束节点的sequencenumber减1,加上1到新的起始节点,说明新旧节点e已经处理完毕,当前正在处理的是新起始节点g和旧结束节点d。5.新端节点(新节点d)与旧端节点(旧节点d)相同。只更新旧节点d的DOM内容。新端节点序号减1,旧端节点序号减1,表示新旧节点d已经处理完毕,新端节点g和旧端节点c目前正在处理中。由于旧节点c为空,所以旧的结束节点为b。6.新节点的起始节点和结束节点与旧节点不同,在旧节点中没有找到与新起始节点(新节点g)相同的节点。于是创建节点g的DOM,并添加到老起始节点(老节点a)的DOM前面,然后在新起始节点的序号上加1,说明新起始节点g已经processed,当前正在处理新的起始节点d。7.由于新的起始节点和结束节点的序号重叠,新节点已经处理完,还有旧节点没有处理,则去除未处理的旧节点DOM。8.结束,最后的DOM。