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
functionupdateChildren(parentElm,oldCh,newCh,insertedVnodeQueue,removeOnly){letoldStartIdx=0;// 旧节点开始位置letnewStartIdx=0;// 新节点开始位置letoldEndIdx=oldCh.length-1;// 旧节点结束位置letoldStartVnode=oldCh[0];// 旧节点未处理的第一个节点(旧头)letoldEndVnode=oldCh[oldEndIdx];// 旧节点未处理的最后一个节点(旧尾)letnewEndIdx=newCh.length-1;// 新节点结束位置letnewStartVnode=newCh[0];// 新节点未处理的第一个节点(新头)letnewEndVnode=newCh[newEndIdx];// 新节点未处理的最后一个节点(新尾)letoldKeyToIdx,idxInOld,vnodeToMove,refElm//...//遍历 oldCh 和 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递归下去patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)//旧头和新头位置同时右移oldStartVnode=oldCh[++oldStartIdx]newStartVnode=newCh[++newStartIdx]}elseif(sameVnode(oldEndVnode,newEndVnode)){//同位置比较,如果旧尾和新尾相同时,继续执行patchVnode递归下去patchVnode(oldEndVnode,newEndVnode,insertedVnodeQueue,newCh,newEndIdx)//旧尾和新尾位置同时左移oldEndVnode=oldCh[--oldEndIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldStartVnode,newEndVnode)){//不同位置比较(需要移动),如果旧头和新尾相同时,先执行patchVnode递归下去,再执行insertBefore插入相应位置的真实dom节点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)){//不同位置比较(需要移动),如果旧尾和新头相同时,先执行patchVnode递归下去,再执行insertBefore插入相应位置的真实dom节点patchVnode(oldEndVnode,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)canMove&&nodeOps.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm)oldEndVnode=oldCh[--oldEndIdx]newStartVnode=newCh[++newStartIdx]}else{//如果新旧头尾都不同时,建立key-->index的对应关系,判断新节点是否在旧节点的集合中,有就移动相应位置,否则直接创建新的节点插入if(isUndef(oldKeyToIdx))oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx)// 如果 oldKeyToIdx 不存在,创建 old children 中 vnode 的 key 到 index 的// 映射,方便我们之后通过 key 去拿下标。idxInOld=isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode,oldCh,oldStartIdx,oldEndIdx)//判断新节点是否在旧节点中,并获取相应索引下标if(isUndef(idxInOld)){//如果不存在则直接新建一个真实dom节点createElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx)}else{//存在则先判断两个节点是否相同vnodeToMove=oldCh[idxInOld]//根据索引下标获取旧节点if(sameVnode(vnodeToMove,newStartVnode)){//当两个节点相等时,执行patchVnode递归下去,再执行insertBefore插入相应位置的真实dom节点patchVnode(vnodeToMove,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)oldCh[idxInOld]=undefinedcanMove&&nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm)}else{//相同key但是节点不同时,直接创建新的节点// same key but different element. treat as new elementcreateElm(newStartVnode,insertedVnodeQueue,parentElm,oldStartVnode.elm,false,newCh,newStartIdx)}}newStartVnode=newCh[++newStartIdx]}}//上面循环结束后,处理可能未处理到的节点if(oldStartIdx>oldEndIdx){//新节点还有未处理的,遍历剩余的新节点并逐个新增到真实DOM中refElm=isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elmaddVnodes(parentElm,refElm,newCh,newStartIdx,newEndIdx,insertedVnodeQueue)}elseif(newStartIdx>newEndIdx){//旧节点还有未处理完的,删除对应的domremoveVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx)}}
Diff算法
Diff
算法的前提是发生在相同层级,对比新旧vnode子节点,简化复杂度,时间复杂度为O(n)。下面用一张示例图来展示:
图上同颜色线框会进行比较:
a
节点,由于新旧vnode
树都是a
,才会继续对其子节点进行比较vnode
树中是节点b
和c
,旧vnode
树是节点b
和g
,则会删除节点g
,添加节点c
,由于拥有相同的节点b
,继续对b
节点的子节点进行比较vnode
树中只有d
节点,则删除旧vnode
树中e
节点最后我们可以得知只会在相同层级进行
Diff
,只有在相同层级且相同的情况下才会对其子节点进行比较,多余的相同层级节点只会作删除或添加操作源码分析
当响应式数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图。
直接从更新方法
vm._update
开始分析,它的定义在src/core/instance/lifecycle.js
中:每当响应式数据发生改变时,都会先判断有没有旧
VNode
,没有就传入真实的dom
节点直接执行vm.__patch__
,否则传入prevVnode
和vnode
进行diff,完成更新工作。上面
_update
函数中有个VNode
的参数,在Vue.js
中,Virtual DOM
是用VNode
这个Class
去描述,我们看看源码是怎么定义的,它定义在src/core/vdom/vnode.js
中:这里我们只需要了解几个核心的属性就行了,例如:
tag
属性即节点标签属性data
属性是一个存储节点属性的对象,节点上的class
,style
以及绑定的事件children
属性是vnode
的子节点集合,每个子结点也是vnode
结构text
属性是文本节点elm
属性为这个vnode
对应的真实dom
节点的引用key
属性是vnode
的标记,在diff
过程中对比key
可以提高diff
的效率接着看
vm.__patch__
,vm.__patch__
方法定义在src/core/vdom/patch.js
中:此时对于
patch
过程有了大概的了解,我们来总结一下吧:oldVnode
不存在时,根据vnode
直接创建新的dom
节点oldVnode
存在时,首先判断是不是真实的元素,再调用sameVnode
比较新旧节点是否相同。oldVnode
和vnode
基本属性相同时,则调用patchVnode
进行递归比较oldVnode
和vnode
基本属性不相同时,则根据vnode
创建新的真实dom
,同时根据oldVnode
删除旧dom节点,上面代码中有看到
sameVnode
和patchVnode
两个方法,接下来我们来看看这两个方法有什么软用:sameVnode
主要是以下几个属性进行比较:
patchVnode
接着重头戏
patchVnode
方法,来看看源码是怎么实现的吧。从以上源码得知
diff
过程:vnode
是否是文本节点,若oldVnode.text !== vnode.text
,用vnode
的文本替换真实dom
节点的内容vnode
没有文本节点时,则开始进行子节点的比较vnode
的子节点和oldVnode
的子节点都存在且不相同的情况下,递归调用updateChildren
进行更新子节点vnode
的子节点存在,oldVnode
有文本时,清空dom中的文本,同时调用addVnodes
方法把vnode
的子节点添加到真实dom中oldVnode
的子节点存在时,则直接清空真实dom
下对应的oldVnode
子节点oldVnode
存在且有文本节点,直接清空对应的文本updateChildren
上面提到了
updateChildren
方法,这才是diff
的核心算法,我们一起来看下到底干了什么:具体
diff
过程分析:保存四个变量
oldStartIdx
、oldEndIdx
、newStartIdx
、newEndIdx
来作为遍历的索引,当oldCh
或者newCh
的startIndex > endIndex
,循环结束。接下来我们来一一分析:
patchVnode
操作,头部索引向右移动patchVnode
操作,尾部索引向左移动patchVnode
操作,旧头索引向右移动,新尾索引向左移动patchVnode
操作,旧尾索引向左移动,新头索引向右移动oldCh
数组建立key --> index
的 map映射。newStartVnode
(简化逻辑,有循环我们最终还是会处理到所有vnode
),通过它的key
从上面的map
里拿到index
;index
不存在,那么说明newStartVnode
是全新的vnode
,直接创建对应的
dom
并插入。index
存在,并且是相同的节点,继续进行patchVnode
操作,再执行insertBefore
插入相应位置的真实dom
节点;index
存在,并且是不同的节点,直接创建对应的
dom
并插入;我们再通过示意图来理解以上过程:
首先进行旧头新头比较,都是
A
,所以双方头部索引向右移旧头新头再继续比较,发现不一样,
B!==C
,所以进入旧尾新尾比较,D===D
,双方尾部索引向左移现在进入新的循环,旧头新头比较,
B!==C
,旧尾新尾比较,C!==E
,进入头尾交叉比较,先进行旧尾新头比较C===C
,旧尾索引左移,新头索引右移紧接着再进入新一轮的循环,旧头新头比较,
B!==F
,旧尾新尾比较,B!==E
,头尾交叉比较,B!==E
,B!==F
,四种都不相同,这个时候需要通过key
去比对,然后将新头右移,重复循环直至任一头部索引大于尾部索引,循环结束。上述循环结束后,可能存在未处理的vnode
oldStartIdx > oldEndIdx
,说明oldCh
先处理完,newCh
还有未处理完的,添加newCh
中未处理的节点newStartIdx > newEndIdx
,说明oldCh
未处理完,删除(oldCh
中对应的)多余的dom参考文献
The text was updated successfully, but these errors were encountered: