updateChildren()
/* diff 过程: diff 优化: 1. 四种假设: newStart === oldStart newEnd === oldEnd newStart === oldEnd newEnd === oldStart 2. 假设新老节点开头结尾有相同节点的情况: - 一旦命中假设,就避免了一次循环,以提高执行效率 - 如果没有命中假设,则执行遍历,从老节点中找到新开始节点 找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置 如果老节点先于新节点遍历结束,则剩余的新节点执行新增节点操作 如果新节点先于老节点遍历结束,则剩余的老节点执行删除操作,移除这些老节点 */ function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { // 老节点的开始索引 let oldStartIdx = 0 // 新节点的开始索引 let newStartIdx = 0 // 老节点的结束索引 let oldEndIdx = oldCh.length - 1 // 老节点的第一个子节点 let oldStartVnode = oldCh[0] // 老节点的最后一个子节点 let oldEndVnode = oldCh[oldEndIdx] // 新节点的结束索引 let newEndIdx = newCh.length - 1 // 新节点的第一个子节点 let newStartVnode = newCh[0] // 新节点的最后一个子节点 let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly 是一个特殊的标志,仅由 <transition-group> 使用, // 以确保被移除的元素在离开转换期间保持在正确的相对位置 const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { // 检查新节点的 key 是否重复 checkDuplicateKeys(newCh) } // 遍历新老两组节点,只要有一组遍历完(开始索引超过结束索引)则跳出循环 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { // 如果节点被移动,在当前索引上可能不存在,检测这种情况,如果节点不存在则调整索引 oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { // 如果节点被移动,在当前索引上可能不存在,检测这种情况,如果节点不存在则调整索引 oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // 老开始节点和新开始节点是同一个节点,执行 patch patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // patch 结束后老开始和新开始的索引分别加 1,开始下一个节点 oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { // 老结束和新结束是同一个节点,执行 patch patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) // patch 结束后老结束和新结束的索引分别减 1,开始下一个节点 oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // 老开始和新结束是同一个节点,执行 patch patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) // 处理被 transtion-group 包裹的组件时使用 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) // patch 结束后老开始索引加 1,新结束索引减 1,开始下一个节点 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // 老结束和新开始是同一个节点,执行 patch patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) // patch 结束后,老结束的索引减 1,新开始的索引加 1,开始下一个节点 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 如果上面的四种假设都不成立,则通过遍历找到新开始节点在老节点中的位置索引 // 找到老节点中每个节点 key 和 索引之间的关系映射: // 如 oldKeyToIdx = { key1: idx1, ... } if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 在映射中找到新开始节点在老节点中的位置索引 idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element // 在老节点中没找到新开始节点,则说明是新创建的元素,执行创建 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // 在老节点中找到新开始节点了 vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { // 如果这两个节点是同一个,则执行 patch patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // patch 结束后将该老节点置为 undefined oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element // 最后这种情况是,找到节点了,但是发现两个节点不是同一个节点, // 则视为新元素,执行创建 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } // 老节点向后移动一个 newStartVnode = newCh[++newStartIdx] } } // 走到这里,说明老节点或者新节点被遍历完了 if (oldStartIdx > oldEndIdx) { // 老节点被遍历完了,新节点有剩余,则说明这部分剩余的节点是新增的节点,然后添加这些节点 refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 新节点被遍历完了,老节点有剩余,说明这部分的节点被删掉了,则移除这些节点 removeVnodes(oldCh, oldStartIdx, oldEndIdx) } } 复制代码
checkDuplicateKeys()
// 检查一组元素的 key 是否重复 function checkDuplicateKeys(children) { const seenKeys = {} for (let i = 0; i < children.length; i++) { const vnode = children[i] const key = vnode.key if (isDef(key)) { if (seenKeys[key]) { warn( `Duplicate keys detected: '${key}'. This may cause an update error.`, vnode.context ) } else { seenKeys[key] = true } } } } 复制代码
addVnodes()
// 在指定索引范围(startIdx —— endIdx)内添加节点 function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) { for (; startIdx <= endIdx; ++startIdx) { createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx) } } 复制代码
createKeyToOldIdx()
// 得到指定范围(beginIdx —— endIdx)内节点的 key 和 索引之间的关系映射 => { key1: idx1, ... } function createKeyToOldIdx(children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map } 复制代码
findIdxInOld()
// 找到新节点(vnode)在老节点(oldCh)中的位置索引 function findIdxInOld(node, oldCh, start, end) { for (let i = start; i < end; i++) { const c = oldCh[i] if (isDef(c) && sameVnode(node, c)) return i } } 复制代码
invokeCreateHooks()
/* 调用各个模块的 create 方法,比如创建属性的、创建样式的、指令的等等, 然后执行组件的 mounted 生命周期方法 */ function invokeCreateHooks(vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } // 组件钩子 i = vnode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) // 调用组件的 insert 钩子,执行组件的 mounted 生命周期方法 if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } } 复制代码
createChildren()
// 创建所有子节点,并将子节点插入父节点,形成一棵 DOM 树 function createChildren(vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { // children 是数组,表示是一组节点 if (process.env.NODE_ENV !== 'production') { // 检测这组节点的 key 是否重复 checkDuplicateKeys(children) } // 遍历这组节点,依次创建这些节点然后插入父节点,形成一棵 DOM 树 for (let i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) } } else if (isPrimitive(vnode.text)) { // 说明是文本节点,创建文本节点,并插入父节点 nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } } 复制代码
总结
Vue 中的 patch 都做了些什么?Vue 的 patch 负责组件的 首次渲染、后续更新、销毁组件
- 如果老的
VNode
是真实元素,则表示首次渲染,创建整棵DOM
树,并插入body
,然后移除老的模版节点 - 如果老的
VNode
不是真实元素,并且新的VNode
也存在,则表示更新阶段,执行patchVnode
- 首先是全量更新所有的属性
- 如果新老
VNode
都有孩子,则递归执行updateChildren
,进行diff
过程 - 老的
VNode
没孩子,如果新的VNode
有孩子,则新增这些新孩子节点 - 如果老的
VNode
有孩子,新的VNode
没孩子,则删除这些老孩子节点 - 如果不符合上面的几种,说明属于更新文本节点
- 如果新的
VNode
不存在,老的VNode
存在,则调用destroy
销毁老节点
diff 过程结合 DOM 特点的优化?
- 同层比较(降低时间复杂度)深度优先(递归)
- 根据
DOM
节点做了四种假设,假设新老VNode
的开头结尾存在相同节点
- 如果命中假设,就可避免一次循环,降低了
diff
时间复杂度,提高执行效率 - 如果没有命中假设,则执行遍历,从老的
VNode
中找到新的VNode
的开始节点
- 找到相同节点,则执行
patchVnode
,然后将老节点移动到正确的位置 - 如果老的
VNode
先于新的VNode
遍历结束,则剩余的新的VNode
执行新增节点操作 - 如果新的
VNode
先于老的VNode
遍历结束,则剩余的老的VNode
执行删除操作,移除老节点