vue diff 算法学习
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 is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { // oldStartVnode没有,则oldStartIdx后移一位 oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { // oldEndVnode没有,则oldEndIdx前移一位 oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // 处理 头部 的同类型节点,即oldStartVnode和newStartVnode指向同类节点的情况 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // 标记: oldStartIdx 和 newStartIdx 后移1位 } else if (sameVnode(oldEndVnode, newEndVnode)) { // 处理 尾部 的同类型节点,即oldEndVnode和newEndVnode指向同类节点的情况 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // 标记: oldEndIdx 和 newEndIdx 前移1位 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right 处理 头尾 同类型节点,即oldStartVnode和newEndVnode指向同类节点的情况 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) // 把oldStartVnode的el节点元素右移到oldEndVnode的el节点后面紧跟节点元素的前面(说白了就是oldEndVnode的el节点的后面) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // 标记: oldStartIdx 后移1位, newEndIdx 前移1位 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left 处理 尾头 的同类型节点,即oldEndVnode和newStartVnode指向同类节点的情况 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) // 把oldEndVnode的el节点左移到oldStartVnode的el节点的前面 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] // 标记: oldEndIdx 前移1位 newStartIdx 后移1位 } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // oldKeyToIdx一个对象,存储剩下oldCh的键值对: {key: 索引} idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 该三目是为了查找newStartVnode在oldCh的索引 if (isUndef(idxInOld)) { // New element undefined的话,说明newStartVnode未在oldCh中找到,说明它是一个新增的节点,则创建一个新的节点,且插入到oldStartVnode的elm dom的前面 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // 找到newStartVnode在oldCh中的索引,说明是需要处理更新的节点 vnodeToMove = oldCh[idxInOld] // 在oldCh中找到需要移动的vnode if (sameVnode(vnodeToMove, newStartVnode)) { // 如果需要移动的vnode和newStartVnode是同类节点,则把vnodeToMove的el dom移动到oldStartVnode的el dom的前面,且把刚才移动的vnode标记为undefined patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) oldCh[idxInOld] = undefined // 标记为undefined目的是: canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果key相同,元素不同,则视为新增元素 // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] // 如果是处理新增节点的情况: oldCh中没有新增节点,所以标记过程中它的指针不需要移动,只需要把newStartIdx后移1位 // 如果是处理更新的节点的情况: 在oldCh中该节点不在指针处,所以采用设置为undefined的方式来标记,但是newCh中的newStartIdx后移1位 } } if (oldStartIdx > oldEndIdx) { // 当oldCh中的起止点相遇了,但是新vnode中的起止点没有相遇,这时需要对新vnode中的未处理节点进行处理,这类节点属于更新中被加入的节点,需要将他们插入到DOM树中 refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 当新vnode中的起止点相遇,且newStartIdx超过newEndIdx,需要把oldCh中oldStartIdx和oldEndIdx之间(包含他们)的dom删除(但是oldCh还有undefined标记的则不需要删除) removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
配合以下demo来看这段代码
const res = Vue.compile(` <div> <p v-for="(item, index) in arr" :key="item" > {{ item }} </p> </div> `) const vm = new Vue({ data: { arr: Array.apply(null, { length: 10 }).map((item, index) => { return index + 1 }) }, methods: { switchArr() { this.arr = [1, 9, 11, 7, 3, 4, 5, 6, 2, 10] } }, render: res.render, staticRenderFns: res.staticRenderFns }).$mount('#app')
1.处理头部相同的节点
else if (sameVnode(oldStartVnode, newStartVnode)) { // 处理 头部 的同类型节点,即oldStartVnode和newStartVnode指向同类节点的情况 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // 标记: oldStartIdx 和 newStartIdx 后移1位 }
2.处理 尾部 的同类型节点
else if (sameVnode(oldEndVnode, newEndVnode)) { // 处理 尾部 的同类型节点,即oldEndVnode和newEndVnode指向同类节点的情况 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // 标记: oldEndIdx 和 newEndIdx 前移1位 }
3.处理 头尾 同类型节点
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right 处理 头尾 同类型节点,即oldStartVnode和newEndVnode指向同类节点的情况 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) // 把oldStartVnode的el节点元素右移到oldEndVnode的el节点后面紧跟节点元素的前面(说白了就是oldEndVnode的el节点的后面) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // 标记: oldStartIdx 后移1位, newEndIdx 前移1位 }
4.处理 尾头 的同类型节点
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left 处理 尾头 的同类型节点,即oldEndVnode和newStartVnode指向同类节点的情况 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) // 把oldEndVnode的el节点左移到oldStartVnode的el节点的前面 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] // 标记: oldEndIdx 前移1位 newStartIdx 后移1位 }
5. 处理新增节点
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // oldKeyToIdx一个对象,存储剩下oldCh的键值对: {key: 索引} idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 该三目是为了查找newStartVnode在oldCh的索引 if (isUndef(idxInOld)) { // New element undefined的话,说明newStartVnode未在oldCh中找到,说明它是一个新增的节点,则创建一个新的节点,且插入到oldStartVnode的elm dom的前面 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) }
6.处理需要更新的节点
if (isUndef(idxInOld)) { // New element undefined的话,说明newStartVnode未在oldCh中找到,说明它是一个新增的节点,则创建一个新的节点,且插入到oldStartVnode的elm dom的前面 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // 找到newStartVnode在oldCh中的索引,说明是需要处理更新的节点 vnodeToMove = oldCh[idxInOld] // 在oldCh中找到需要移动的vnode if (sameVnode(vnodeToMove, newStartVnode)) { // 如果需要移动的vnode和newStartVnode是同类节点,则把vnodeToMove的el dom移动到oldStartVnode的el dom的前面,且把刚才移动的vnode标记为undefined patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) oldCh[idxInOld] = undefined // 标记为undefined目的是: canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果key相同,元素不同,则视为新增元素 // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] // 如果是处理新增节点的情况: oldCh中没有新增节点,所以标记过程中它的指针不需要移动,只需要把newStartIdx后移1位 // 如果是处理更新的节点的情况: 在oldCh中该节点不在指针处,所以采用设置为undefined的方式来标记,但是newCh中的newStartIdx后移1位
7.继续处理头部相同的节点
8.处理oldCh中未处理的节点删除
if (oldStartIdx > oldEndIdx) { // 当oldCh中的起止点相遇了,但是新vnode中的起止点没有相遇,这时需要对新vnode中的未处理节点进行处理,这类节点属于更新中被加入的节点,需要将他们插入到DOM树中 refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 当新vnode中的起止点相遇,且newStartIdx超过newEndIdx,需要把oldCh中oldStartIdx和oldEndIdx之间(包含他们)的dom删除(但是oldCh还有undefined标记的则不需要删除) removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) }
至此diff算法结束了。
开心的做一个无忧无虑的码农,争取每天进步一点。