组件页面渲染时,将render返回的新vnode(新节点)和组件实例保存的vnode(旧节点)作为参数,调用patch方法,更新DOM。
判断两个节点是否相同
处理过程中,需要判断节点是否相同。相同节点需要满足以下条件:
key相同
标签类型相同
注释节点标识相同,都是注释节点,或者都不是注释节点
data的值状态相同,或者都有值,或者都没值
function sameVnode (a, b) {// 判断两个VNode节点是否是同一个节点 return ( a.key === b.key && // key相同 ( a.tag === b.tag && // tag相同 a.isComment === b.isComment && // 注释节点标识相同 isDef(a.data) === isDef(b.data) && // data值状态相同 sameInputType(a, b) // input的type相同 ) ) }
patch方法
patch判断流程如下:
a) 如果新节点为空,此时旧节点存在(组件销毁时),调用旧节点destroy生命周期函数
b) 如果旧节点为空,根据新节点创建DOM
c) 其他(如果新旧节点都存在)
a) 旧节点不是DOM(组件节点),且新旧节点相同
执行patchVnode
b) 旧节点是DOM元素或者两个节点不相同
创建新节点DOM,销毁旧节点以及DOM。
function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); } return } ... if (isUndef(oldVnode)) { isInitialPatch = true;// 组件初始加载 createElm(vnode, insertedVnodeQueue); } else { var isRealElement = isDef(oldVnode.nodeType); if (!isRealElement && sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); } else { ... var oldElm = oldVnode.elm; var parentElm = nodeOps.parentNode(oldElm);// 获取父元素 // create new node createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm)// 获取紧跟的弟弟元素 ); if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0);// 销毁旧节点以及DOM元素 } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode); } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm } }
patchVnode方法
当两个节点相同时,执行patchVnode方法。在处理各种情况之前,会将旧节点elm属性值赋值给新节点的elm属性,保持elm保持一致。
具体流程如下:
a)如果新旧节点完全相同(引用相同 oldVnode === vnode)
直接返回不处理
b) 如果新节点不是文本节点
a)都存在子节点,新旧节点的子节点数组引用不同(oldCh !== ch)
updateChildren
b)新节点有子节点,旧节点没有
1)查重子节点(key)
2)如果旧节点是文本节点,先清空文本
3)创建子节点DOM元素
c)旧节点有子节点,新节点没有
移除子节点以及DOM
d)旧节点是文本节点
清除文本
c)如果新节点是文本节点,并且和旧节点文本不相同
则直接替换文本内容。
d)其他(新节点是文本节点,并且和旧节点相同)
不处理
function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { if (oldVnode === vnode) { return } ... if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); } } else if (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); } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text); } ... }
updateChildren方法
updateChildren方法处理相同新旧节点的子节点。方法定义了以下变量(updateChildren的节点都表示的是子节点):
var oldStartIdx = 0;// 表示当前正在处理的旧起始节点序号 var newStartIdx = 0;// 表示当前正在处理的新起始节点序号 var oldEndIdx = oldCh.length - 1;// 表示当前正在处理的旧结尾节点序号 var oldStartVnode = oldCh[0];// 表示当前正在处理的旧起始节点 var oldEndVnode = oldCh[oldEndIdx];// 表示当前正在处理的旧结尾节点 var newEndIdx = newCh.length - 1;// 表示当前正在处理的新结尾节点序号 var newStartVnode = newCh[0];// 表示当前正在处理的新起始节点 var newEndVnode = newCh[newEndIdx];// 表示当前正在处理的新结尾节点 var oldKeyToIdx, // 尚未处理的旧节点key值映射 idxInOld, // 与新节点key值相同的旧节点序号 vnodeToMove, // 与新节点key值相同的旧节点 refElm;// 指向当前正在处理的新结尾节点的后一个节点(已处理)的DOM元素