You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Vue.prototype._update=function(vnode: VNode,hydrating?: boolean){constvm: Component=thisconstprevEl=vm.$elconstprevVnode=vm._vnodeconstprevActiveInstance=activeInstanceactiveInstance=vmvm._vnode=vnode// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used.if(!prevVnode){// initial rendervm.$el=vm.__patch__(vm.$el,vnode,hydrating,false/* removeOnly */)}else{// updatesvm.$el=vm.__patch__(prevVnode,vnode)}// 省略}
returnfunctionpatch(oldVnode,vnode,hydrating,removeOnly,parentElm,refElm){if(isUndef(vnode)){if(isDef(oldVnode)){invokeDestroyHook(oldVnode);}return}varisInitialPatch=false;varinsertedVnodeQueue=[];if(isUndef(oldVnode)){// empty mount (likely as component), create new root elementisInitialPatch=true;createElm(vnode,insertedVnodeQueue,parentElm,refElm);}else{varisRealElement=isDef(oldVnode.nodeType);if(!isRealElement&&sameVnode(oldVnode,vnode)){//比较元素类型// patch existing root nodepatchVnode(oldVnode,vnode,insertedVnodeQueue,removeOnly);//相同则比较元素}else{// 省略oldVnode=emptyNodeAt(oldVnode);}// replacing existing elementvaroldElm=oldVnode.elm;varparentElm$1=nodeOps.parentNode(oldElm);// create new node// 不同则直接创建新的createElm(vnode,insertedVnodeQueue,// extremely rare edge case: do not insert if old element is in a// leaving transition. Only happens when combining transition +// keep-alive + HOCs. (#4590)oldElm._leaveCb ? null : parentElm$1,nodeOps.nextSibling(oldElm));// 省略 parent相关内容}}invokeInsertHook(vnode,insertedVnodeQueue,isInitialPatch);returnvnode.elm}
functionupdateChildren(parentElm,oldCh,newCh,insertedVnodeQueue,removeOnly){letoldStartIdx=0letnewStartIdx=0letoldEndIdx=oldCh.length-1letoldStartVnode=oldCh[0]letoldEndVnode=oldCh[oldEndIdx]letnewEndIdx=newCh.length-1letnewStartVnode=newCh[0]letnewEndVnode=newCh[newEndIdx]letoldKeyToIdx,idxInOld,vnodeToMove,refElm// removeOnly is a special flag used only by <transition-group>// to ensure removed elements stay in correct relative positions// during leaving transitionsconstcanMove=!removeOnlyif(process.env.NODE_ENV!=='production'){checkDuplicateKeys(newCh)}while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){if(isUndef(oldStartVnode)){oldStartVnode=oldCh[++oldStartIdx]// Vnode has been moved left}elseif(isUndef(oldEndVnode)){oldEndVnode=oldCh[--oldEndIdx]}elseif(sameVnode(oldStartVnode,newStartVnode)){patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue)oldStartVnode=oldCh[++oldStartIdx]newStartVnode=newCh[++newStartIdx]}elseif(sameVnode(oldEndVnode,newEndVnode)){patchVnode(oldEndVnode,newEndVnode,insertedVnodeQueue)oldEndVnode=oldCh[--oldEndIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldStartVnode,newEndVnode)){// Vnode moved rightpatchVnode(oldStartVnode,newEndVnode,insertedVnodeQueue)canMove&&nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))oldStartVnode=oldCh[++oldStartIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldEndVnode,newStartVnode)){// Vnode moved leftpatchVnode(oldEndVnode,newStartVnode,insertedVnodeQueue)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)){// New elementcreateElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx)}else{vnodeToMove=oldCh[idxInOld]if(sameVnode(vnodeToMove,newStartVnode)){patchVnode(vnodeToMove,newStartVnode,insertedVnodeQueue)oldCh[idxInOld]=undefinedcanMove&&nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm)}else{// same key but different element. treat as new elementcreateElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx)}}newStartVnode=newCh[++newStartIdx]}}if(oldStartIdx>oldEndIdx){refElm=isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elmaddVnodes(parentElm,refElm,newCh,newStartIdx,newEndIdx,insertedVnodeQueue)}elseif(newStartIdx>newEndIdx){removeVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx)}}
因为我们此时发现,只有最后两个节点 C 是相同的,因此进入对C节点的递归比较,如果此节点有更新,会按照我们前面所讲的内容在 patchVnode 中对其进行更新。
那么更新C节点之后,oldEndIdx 和 newEndIdx 都会向前移动一步,变成这样:
此时再进行比较,会发现旧孩子的第一个和新孩子的最后一个相同,那么说明这两个被交换了,进入这段代码:
while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){if(isUndef(oldStartVnode)){//省略}elseif(sameVnode(oldStartVnode,newEndVnode)){// Vnode moved rightpatchVnode(oldStartVnode,newEndVnode,insertedVnodeQueue)canMove&&nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))oldStartVnode=oldCh[++oldStartIdx]newEndVnode=newCh[--newEndIdx]}}
functioncreateElm(vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index){// 省略if(createComponent(vnode,insertedVnodeQueue,parentElm,refElm)){return}constdata=vnode.dataconstchildren=vnode.childrenconsttag=vnode.tagif(isDef(tag)){vnode.elm=vnode.ns
? nodeOps.createElementNS(vnode.ns,tag)
: nodeOps.createElement(tag,vnode)setScope(vnode)/* istanbul ignore if */if(__WEEX__){//省略weex}else{createChildren(vnode,children,insertedVnodeQueue)if(isDef(data)){invokeCreateHooks(vnode,insertedVnodeQueue)}insert(parentElm,vnode.elm,refElm)}if(process.env.NODE_ENV!=='production'&&data&&data.pre){creatingElmInVPre--}}elseif(isTrue(vnode.isComment)){vnode.elm=nodeOps.createComment(vnode.text)insert(parentElm,vnode.elm,refElm)}else{vnode.elm=nodeOps.createTextNode(vnode.text)insert(parentElm,vnode.elm,refElm)}}
diff 原则
让我们回顾下,当
vm
上有更新发生时,会触发这一段代码:在上一章我们知道了Vue是如何生成
vnode
的,也就是_render()
函数的工作原理。这一章我们来看看Vue
是如何把vnode
渲染为真实DOM的,这一过程,我们称之为patch
(补丁)._update
函数的定义如下:core/instance/lifecycle.js
这里除了对
prevVnode
以及prevEl
的定义外,我们这里重点关心的是这两段:他们的主要区别是
prevVnode
是否存在,显然,如果是第一次渲染,那么是不存在的,从第二次开始就存在了。由于我们每次patch的时候,入口肯定是根节点,因此第一次渲染的时候,prevVnode
就变成了我们要挂载的那个DOM元素,一般是<div id="app"></div>
。在我们看
patch
算法之前,这里必须提到react对Diff算法的说明:https://reactjs.org/docs/reconciliation.html 。由于对一颗树进行完整的diff需要的时间复杂度是O(n^3)
,所以我们必须有一些妥协来加快速度。React中提到了两个假设,在Vue中同样适用:这个假设有点难以理解,通俗点说就是:
而所有的修改类型总结起来无非是这么几种:
p
替换span
则会删除span
并重新创建一个p
textContent
即可patch 算法
弄清楚了这些原则,让我们进入
patch
的代码看看:core/vdom/patch.js
省略了一些代码之后,
patch
函数的代码依然还是比较长的。眼见的童鞋可能注意到了,这里怎么没有flow
呢?因为这是引入了一个第三方的库,它本来就没用、那么让我们一段段来看:
这里面的
isRealElement
其实就是根据有没有nodeType
来判断是不是真实DOM,vnode 是不存在这个字段的。如果不是真实DOM元素,并且这两个节点是相同的,那就就会进入这个if
内部,进行patchVnode
,这个函数其实主要就是对children进行diff以决定该如何更新。那么什么叫相同节点呢?我们看看sameVnode
的代码:core/vdom/patch.js
这里的判断条件其实主要是两个:
key
必须相同(都是undefined
则也是相同的),div
如果满足以上条件,那么就认为是相同的vnode,因此就可以进行
patchVnode
操作。那么如果不是呢?就认为是完全新的一个vnode
,因此会进入下面的createElm
。让我们梳理下逻辑:当进入patch
之后有两种分支可以走:div
变p
了),那么直接createElm
创建新的DOM元素patchVnode
进行diff,有条件的更新以提升性能这样其实就实现了
React
中原则的第一条,即:两个不同类型的元素将产生不同的树。只要发现两个元素的类型不同,我们直接删除旧的并创建一个新的,而不是去递归比较。那么如果发现他们相同呢?这时候就需要做两件事:
因此
patchVnode
显然实现了这两部分,那么让我们来看看patchVnode
的代码:core/vdom/patch.js
这里我们一段一段来看代码,首先是:
这里的
cbs
其实是从hooks
中来的,hooks
定义如下:从名字可以看出他们是在 vnode 更新的各个阶段进行相应的操作。这里,
cbs.update
包含如下几个回调:可以看到都是对当前元素本身的一些更新操作。至于这些函数内部都做了什么,这里暂且不去深究,其实从名字已经基本可以看出他们的操作内容了。
那么在更新完当前 vnode 节点之后,对于它的孩子们显然也要更新。我们看看对孩子的更新代码:
core/vdom/patch.js
这时候分两种情况:
对孩子是vnode的三种情况:
updateChildren
比较他们的差异,而这一部分就是diff算法的精髓所在弄懂了这部分,那么下面让我们看看
updateChildren
是如何对孩子们的差异进行比较的updateChildren 孩子差异比较算法,diff算法的核心
当新旧节点都有孩子的时候,我们就需要进行孩子的比较。对于每一个孩子节点,我们依然有这么几种可能:
Vue对新旧两个children数组分别在首位各用了一个指针,总共四个指针,由于指针仅仅对数组进行了一次遍历,因此时间复杂度是 O(n),这是非常小的开销。下面我们举个例子来说明如何进行diff的。
updateChildren完整代码如下:
core/vdom/patch.js
假设我们有三个节点
A,B,C
,我们更新之后把A,B
位置调换了,变成了B,A,C
,起始状态如下图所示:现在我们进行第一次循环,我们的代码会进入这一个条件:
因为我们此时发现,只有最后两个节点
C
是相同的,因此进入对C节点的递归比较,如果此节点有更新,会按照我们前面所讲的内容在patchVnode
中对其进行更新。那么更新C节点之后,
oldEndIdx
和newEndIdx
都会向前移动一步,变成这样:此时再进行比较,会发现旧孩子的第一个和新孩子的最后一个相同,那么说明这两个被交换了,进入这段代码:
那么除了更新
oldStartVnode
和newEndVnode
之外,最重要的操作是要把oldStartIdx
和oldEndIdx
交换下位置,这样我们旧的孩子就摇身一变,成了新的孩子相同的结构了:这样我们再次移动只针的时候,会发现
oldStartIdx
已经移动到oldEndIdx
右边了。上面这个例子我们可以理解
更新节点
和移动节点
两中情况,那么如果增加了节点和删除了节点呢?先看增加节点的请款,如下图所示,假设我们增加了一个D节点:
第一次循环的时候,显然开始的两个节点都是相同的,也就是会进入如下代码:
那么指针将会向右移动,变成这样:
此时会发现和前面的情况差不多,B节点依然是相同的节点,因此会再次向右移动指针,就变成了这样:
旧孩子的两个指针已经重叠了,此时显然
oldStart
和newStart
不同,而是oldEnd
和newEnd
相同。因此我们这时候会向左移动指针,变成这样:此时已经完成了比较,因为
oldEndIdx
已经移动到oldStartIdx
左边了,不满足循环条件。不是说好了处理新增节点的吗?怎么就退出循环了?别担心,对新增节点和删除节点的处理,都是在循环外面完成的,也就是如下代码:当循环结束的时候,如果
oldStartIdx > oldEndIdx
也就是我们这里的情况,旧孩子的循环先结束,如果新孩子的未遍历完,那么肯定是增加节点了。在addVnodes
中会判断指针情况决定是否增加孩子。同理,如果
newStartIdx > newEndIdx
那么有可能是因为删除了节点,才导致新孩子遍历完了,旧孩子可能还没完的情况,因此调用removeVnodes
来删除节点。当然,如果你定义了
key
,Vue可以直接根据key找到index
进行比较。以上就是
diff
算法的核心内容。createElm 创建真实DOM元素
最后我们说到,无论怎么比较,最终都是调用
createElm
来根据vnode
创建真实的DOM的。当然createElm
并不是简单的创建DOM,让我们再来看createElm
的代码:最关键的地方是这里:
如果是一个组价,那么
createComponent
会返回true
,因此不会进行接下来的操作,如果不是组件,会进行节点创建工作,并会递归对孩子创建节点。而
createComponent
其实会调用$mount
来挂载组件,又回到了我们流程的最开始处。createElm
代码很容易理解,这里不做过多的讲解,在阅读代码的时候主要注意createComponent
启动了一个新的组件挂载流程即可。完整流程图
综合前面讲过的内容,我画了一个组件挂载和更新的完整的流程图如下,希望可以帮助大家理解和记忆:
到此我们弄懂了神秘的
patch
算法,以及他是如何进行Diff的。下一章,让我们看看指令是如何工作的。
下一章 Vue2.x源码解析系列十一:指令
The text was updated successfully, but these errors were encountered: