vue2源码-十、diff算法
diff算法
diff算法的特点就是平级比较,内部采用了双指针方式进行优化,优化了常见的操作。采用了递归比较的方式。
针对一个节点的diff算法
- 先拿出根节点来进行比较如果是同一个节点则比较属性,如果不是同一个节点则直接换成最新的即可。
- 同一个节点比较属性后,复用老节点
比较儿子
- 一方有儿子 一方没有儿子(删除、添加)
- 两方都有儿子
- 优化比较 头头 尾尾 交叉比较
- 就做一个映射表,用新的去映射表中查找此元素是否存在,存在则移动不存在则插入,最后删除多余的
- 这里会有多移动的情况(vue3 最长递增子序列)
源码分析
当数据发生变化时,
set
方法会调用Dep.notify
通知所有的Watcher
,Watcher
就会调用patch
给真实DOm
打补丁,更新相应的视图。function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // 如果新旧节点相同,则直接返回,不需要做任何操作 if (oldVnode === vnode) { return } // 判断vnode是否合法,如果合法,则执行createElm方法创建真实DOM元素。 if (isUndef(vnode.componentInstance) && isDef(vnode.tag)) { if (true) { if (vnode.ns) { // ... } else if (config.ignoredElements.length && config.ignoredElements.some(ignore => { return isRegExp(ignore) ? ignore.test(vnode.tag) : ignore === vnode.tag })) { // ... } } createComponent(vnode, insertedVnodeQueue, parentElm, refElm) return } // 获取新旧节点的DOM元素 const oldElm = oldVnode.elm const elm = vnode.elm // 获取旧节点对应的上下文信息 const oldCtx = oldVnode.context // 递归处理组件类型的节点,更新其子树 if (isDef(vnode.data)) { if (isDef(i = vnode.data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } if (isDef(i = vnode.data.hook) && isDef(i = i.init)) { i(vnode, true /* hydrating */) } } // 判断vnode是否为文本类型节点 if (isTrue(vnode.text)) { if (isDef(oldVnode.text)) { if (oldVnode.text !== vnode.text) { // 更新文本类型节点的内容 nodeOps.setTextContent(elm, vnode.text) } } else if (isDef(oldVnode.children)) { // 如果旧节点有子节点,则删除其所有子节点,并设置text内容 removeVnodes(oldVnode, oldVnode, 0, oldVnode.children.length - 1) } return } // 处理非文本类型节点 if (isDef(vnode.children) && isDef(oldVnode.children)) { if (vnode.children !== oldVnode.children) { // 对比新旧节点的子节点,更新DOM树 updateChildren(parentElm, oldVnode.children, vnode.children, insertedVnodeQueue, removeOnly) } } else if (isDef(vnode.children)) { // 如果只有新节点有子节点,则将新节点的子节点加入到DOM中 if (true) { checkDuplicateKeys(vnode) } if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } addVnodes(elm, null, vnode.children, 0, vnode.children.length - 1, insertedVnodeQueue) } else if (isDef(oldVnode.children)) { // 如果只有旧节点有子节点,则将其所有子节点从DOM中移除 removeVnodes(oldVnode, oldVnode, 0, oldVnode.children.length - 1) } else if (isDef(oldVnode.text)) { // 如果旧节点是文本类型,则将其内容清空 nodeOps.setTextContent(elm, '') } // 执行更新完毕的钩子函数 if (isDef(i = vnode.data) && isDef(i = i.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode) } }
patch
函数前两个参数为为oldVnode
和Vnode
,分别代表新的节点和之前的旧节点,主要做了四个判断:
- 没有新节点,直接触发旧节点的
destory
钩子- 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用
createElm
- 旧节点和新节点一样,通过
sameVnode
判断节点是否一样,一样时,直接调用patchVnode
去处理这两个节点。- 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点。
下面介绍
patchVnode
方法
patchVnode
主要做一下判断:
- 新节点是否是文本节点,如果是,则直接更新
dom
的文本内容为新节点的文本内容。- 新节点和旧节点都有子节点,则处理比较更新子节点。
- 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新的
DOM
,并且添加进父节点。- 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把
DOM
删除子节点不完全一致,则调用
updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 // 旧头索引 let newStartIdx = 0 // 新头索引 let oldEndIdx = oldCh.length - 1 // 旧尾索引 let newEndIdx = newCh.length - 1 // 新尾索引 let oldStartVnode = oldCh[0] // oldVnode的第一个child let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child let newStartVnode = newCh[0] // newVnode的第一个child let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child 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 // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 如果oldVnode的第一个child不存在 if (isUndef(oldStartVnode)) { // oldStart索引右移 oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left // 如果oldVnode的最后一个child不存在 } else if (isUndef(oldEndVnode)) { // oldEnd索引左移 oldEndVnode = oldCh[--oldEndIdx] // oldStartVnode和newStartVnode是同一个节点 } else if (sameVnode(oldStartVnode, newStartVnode)) { // patch oldStartVnode和newStartVnode, 索引左移,继续循环 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // oldEndVnode和newEndVnode是同一个节点 } else if (sameVnode(oldEndVnode, newEndVnode)) { // patch oldEndVnode和newEndVnode,索引右移,继续循环 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // oldStartVnode和newEndVnode是同一个节点 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // patch oldStartVnode和newEndVnode patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) // oldStart索引右移,newEnd索引左移 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // 如果oldEndVnode和newStartVnode是同一个节点 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // patch oldEndVnode和newStartVnode patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) // oldEnd索引左移,newStart索引右移 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] // 如果都不匹配 } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 如果未找到,说明newStartVnode是一个新的节点 if (isUndef(idxInOld)) { // New element // 创建一个新Vnode createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove } else { vnodeToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !vnodeToMove) { warn( 'It seems there are duplicate keys that is causing an update error. ' + 'Make sure each v-for item has a unique key.' ) } // 比较两个具有相同的key的新节点是否是同一个节点 //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。 if (sameVnode(vnodeToMove, newStartVnode)) { // patch vnodeToMove和newStartVnode patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) // 清除 oldCh[idxInOld] = undefined // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm // 移动到oldStartVnode.elm之前 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) // 如果key相同,但是节点不相同,则创建一个新的节点 } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) } } // 右移 newStartVnode = newCh[++newStartIdx] } }
while
循环主要处理了以下五种情景:
- 当新老
VNode
节点的start
相同时,直接patchVnode
,同时新老VNode
节点的开始索引都加 1- 当新老
VNode
节点的end
相同时,同样直接patchVnode
,同时新老VNode
节点的结束索引都减 1- 当老
VNode
节点的start
和新VNode
节点的end
相同时,这时候在patchVnode
后,还需要将当前真实dom
节点移动到oldEndVnode
的后面,同时老VNode
节点开始索引加 1,新VNode
节点的结束索引减 1- 当老
VNode
节点的end
和新VNode
节点的start
相同时,这时候在patchVnode
后,还需要将当前真实dom
节点移动到oldStartVnode
的前面,同时老VNode
节点结束索引减 1,新VNode
节点的开始索引加 1- 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
- 从旧的
VNode
为key
值,对应index
序列为value
值的哈希表中找到与newStartVnode
一致key
的旧的VNode
节点,再进行patchVnode
,同时将这个真实dom
移动到oldStartVnode
对应的真实dom
的前面- 调用
createElm
创建一个新的dom
节点放到当前newStartIdx
的位置