diff算法和虚拟DOM

vue采用递归+双指针方式进行遍历,原则是: 深度优先,同层比较

虚拟DOM

虚拟 DOM 是一个对象,它其实是将真实 DOM 的数据抽取出来,以对象的形式模拟树形结构,使其更加简洁明了。

案例

页面内容

js 复制代码
// template
<div id="app">
  <p className="text"> zcm </p>
</div>

上面的 DOM 通过 vue 生成了下面的虚拟 DOM ,对象中包含了根节点的标签 tag ,key 值,文本信息 text ,节点属性data等等,同时也含有 elm 属性存放真实 DOM ,同时有个 children 数组,存放子节点,子节点的结构也是一致的。 删减过后的虚拟 DOM

js 复制代码
// 删减过后的 虚拟 DOM
{
  "tag": "div", // 标签
  "key": undefined,// key 值
  "elm": div#app, // 真实 DOM
  "text": undefined, // 文本信息
  "data": { attrs: { id: "app" } }, // 节点属性
  "children": [{ // 孩子信息
    "tag": "p",
    "key": undefined,
    "elm": p.text,
    "data": { attrs: { class: "text" } },
    "children": [{
      "tag": undefined,
      "key": undefined,
      "elm": text,
      "text": "zcm",
      "data": undefined,
      "children": []
    }]
  }]
}

Diff 算法大致过程 vue是采用深度优先+双指针方法

在 vue 中,主要是 patch() , patchVnode() 和 updateChildren() 这三个主要方法来实现 diff 的。

  • 当我们vue中的响应式数据变化的时候,就会触发页面更新函数 updateComponent()
  • 此时updateComponent() 就会调用 patch() 方法,在该方法中进行比较是否为相同节点,是的话执行 patchVnode() 方法,开始比较节点差异; 而如果不是相同节点的话,则进行替换操作。
  • 在 patchVnode() 方法中,首先是更新节点属性,然后会判断有没有孩子节点,有的话则执行 updateChildren() 方法,对孩子节点进行比较;如果没有孩子节点,则进行文本内容判断是否更新;(文本节点是不会有孩子节点)
  • updateChildren() 中,会对传入的两个孩子节点数组进行一一比较,找到相同节点的情况下,调用 patchVnode() 继续节点差异比较。

IsDef() 和 isUndef()

IsDef() 该方法判断 vnode 是否存在, 实质上是判断 vnode 是不是 undefined 或 null ,毕竟 vnode 虚拟 DOM 是个对象。

  • IsDef()方法返回不是 undefined和null
  • isUndef()方法返回是 undefined 或者 null

SameVnode

SameVnode() 方法是判断两个节点是否相同,实质上是通过判断 key 值、tag 标签灯静态属性从而去判断两个节点是否为相同节点。

相同的节点不意味着相等节点,比如<div>zcm</div><div>zcq</div>为相同节点,但是它们并不相等。在源码中是通过vnode1 === vnode2去判断是不是为相等节点。

key,tag,isComment,asyncFactory,isDef(x.data)等静态属性

js 复制代码
// 比较是否相同节点
function sameVnode(a, b) {
    return (
        a.key === b.key &&
        a.asyncFactory === b.asyncFactory && (
            (
                a.tag === b.tag &&
                a.isComment === b.isComment &&
                isDef(a.data) === isDef(b.data) &&
                sameInputType(a, b)
            ) || (
                isTrue(a.isAsyncPlaceholder) &&
                isUndef(b.asyncFactory.error)
            )
        )
    )
}

function sameInputType(a, b) {
    if (a.tag !== 'input') return true
    let i
    const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
    const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
    return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

patch() 方法

首先来看下patch() 方法,该方法接收新旧虚拟DOM ,既 oldVnode, vnode, 这个函数其实是对新旧虚拟 Dom 做一个简单的判断,而还没有进入详细的比较阶段。

  • 首先判断 vnode 是否存在,如果不存在的话,则代表这个旧节点要整个删除;
  • 如果 vnode 存在的话,在判断 oldVnode 是否存在,如果不存在的话,则代表只需要新增整个 vnode 节点就可以
  • 如果 vnode 和 oldVnode 都存在的话,判断两者是不是相同节点,如果是的话,这时调用 patchVnode 方法,对两个节点进行详细比较;
  • 如果两者不是相同节点的话,这种情况一般就是初始化页面,此时 oldVnode 其实就是真实 DOM, 这时只需要将 vnode 转换为真实 dom ,然后替换掉 oldVnode
js 复制代码
// 更新时调用的__patch__
function patch(oldVnode, vnode, hydrating, removeOnly) {
    // 判断新节点是否存在
    if (isUndef(vnode)) {
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)  // 新的节点不存在且旧节点存在:删除
        return
    }

		// 判断旧节点是否存在
    if (isUndef(oldVnode)) {
				// 旧节点不存在且新节点存在:新增
        createElm(vnode, insertedVnodeQueue)  
    } else {
        if (sameVnode(oldVnode, vnode)) {
            // 比较新旧节点 diff算法
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
        } else {
            // 初始化页面(此时的oldVnode是个真实DOM)
                oldVnode = emptyNodeAt(oldVnode)
            }
            // 创建新的节点
            createElm(
                vnode,
                insertedVnodeQueue,
                oldElm._leaveCb ? null : parentElm,
                nodeOps.nextSibling(oldElm)
            )
        }
    }

    return vnode.elm
}

patchVnode() 方法

在 patchVnode() 中,同样是接收新旧虚拟DOM,既 oldVnode , vnode; 在该函数中,既开始对两个虚拟 DOM 进行比较了。

  • 首先判断两个虚拟DOM是不是全等,既没有任何变动,是的话直接结束函数,否则继续执行;
  • 其次更新节点的属性
  • 接着判断vnode.text是否存在,既vnode 是不是文本节点,是的话,只需要更新节点文本即可,否则的话,就继续比较。
  • 判断vnode 和oldVnode是否有孩子节点:
    • 如果两者都有孩子节点的话,执行updateChildren()方法,进行比较更新孩子节点
    • 如果vnode 有孩子节点而 oldVnode 没有的话,则直接新增所有孩子节点,并将该节点文本属性设为空
    • 如果oldVnode有孩子节点而vnode没有的话,则直接删除所有孩子节点
    • 如果两者都没有孩子节点,就判断oldVnode.text 是否有内容,有的话清空内容即可
js 复制代码
// 比较两个虚拟DOM
function patchVnode(oldVnode, vnode,  insertedVnodeQueue, ownerArray, index, removeOnly) {
    // 如果两个虚拟DOM一样,无需比较直接返回
    if (oldVnode === vnode) {
        return
    }

    // 获取真实DOM
    const elm = vnode.elm = oldVnode.elm

    // 获取两个比较节点的孩子节点
    const oldCh = oldVnode.children
    const ch = vnode.children

    // 属性更新
    if (isDef(data) && isPatchable(vnode)) {
        for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
        if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }

    if (isUndef(vnode.text)) {   // 没有文本 -> 该情况一般都是有孩子节点
        if (isDef(oldCh) && isDef(ch)) {  // 新旧节点都有孩子节点 -> 比较子节点
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        } else if (isDef(ch)) {  // 新节点有孩子节点,旧节点没有孩子节点 -> 新增
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')  // 如果旧节点有文本内容,将其设置为空
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        } else if (isDef(oldCh)) {   // 旧节点有孩子节点,新节点没有孩子节点 -> 删除
            removeVnodes(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

首先这个方法传入三个比较重要的参数,既 parentElm 父级真实节点,便于直接操作; oldCh 为oldVnode的孩子节点,;newCh 为Vnode 的孩子节点。

oldCh和newCh都是一个数组。这个方法的作用就是对这两个数组一一比较,找到相同的节点,执行patchVnode 再次进行比较更新,剩下的新增或者删除。

在vue 中,主要的实现是用四个指针进行实现,四个指针初始位置分别在两个数组的头尾。因此我们初始化必要的变量

js 复制代码
let oldStartIdx = 0;// oldCh数组左边的指针位置
let oldStartVnode = oldCh[0];// oldCh数组左边的指针对应的节点
let oldEndIdx = oldCh.length - 1;// oldCh数组右边的指针位置
let oldEndVnode = oldCh[oldEndIdx]; // oldCh数组右边的指针对应的节点
let newStartIdx = 0;	// newCh数组左边的指针位置
let newStartVnode = newCh[0];	// newCh数组左边的指针对应的节点
let newEndIdx = newCh.length - 1;// newCh数组右边的指针位置
let newEndVnode = newCh[newEndIdx]; // newCh数组右边的指针对应的节点

接下来进行比较

  • 首先是 oldStartVnode 和 newStartVnode 进行比较,如果比较是相同节点,我们就可以执行 patchVnode(), 并且移动 oldStartIdx和newStartIdx
js 复制代码
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      }
}
  • 如果oldStartVnode和 newStartVnode 匹配不上的话,接下来就是oldEndVnode和newEndVnode做比较了。
js 复制代码
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        // ...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      }
}
  • 但是如果两头比较和两尾都不是相同节点的话,这时候就开始交叉比较了,首先是 oldStartVnode和newEndVnode做比较。
    但是交叉比较的时候,如果匹配上了的话,就需要注意到一个问题,这时候你不仅仅要比较更新节点的内容,你还还需要移动节点的位置,因此我们可以借助 inserBefore 和 nextSibling 的DOM 操作方法去实现。
js 复制代码
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            // 将oldStartVnode节点移动到对应位置,即oldEndVnode节点的后面
            nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
     
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
      }
}
  • 如果oldStartVnode和newEndVnode匹配不上的话,就需要 oldEndVnode 和 newStartVnode 进行比较。
js 复制代码
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldEndVnode, newStartVnode)){
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 将oldEndVnode节点移动到对应位置,即oldStartVnode节点的前面
            nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
      }
}
  • 此时,如果四种方法都匹配不到相同节点的话,剩下的只能使用暴力解法去实现,也就是针对于 newStartVnode 这个节点,我们去遍历 oldCh 中剩余的节点,一一匹配

在 vue 中,我们知道标签会有一个属性 key 值,而在同一级的dom 中,如果 key 有值得话,他必须是唯一值,如果不设置值就默认尾 undefined 。所以我们可以先用 key 来配对一下。

我们可以先 生成一个 oldCh 得key-> index 的映射表,我们可以创建一个函数 createKeyToOldIdx 实现,返回结果用一个变量 oldKeyToIdx 去存储。

js 复制代码
oldKeyToIdx = function createKeyToOldIdx(children, beginIdx, endIdx) {
    let key
    const map = {}
    for (let i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key
        if (isDef(key)) map[key] = i
    }
    return map
}

这个时候,如果 newStartVnode 存在key 的话,我们就可以直接用 oldKeyToIdx[newStartVnode.key] 拿到对应旧孩子节点的下标 index

但如果 newStartVnode 没有 key 值得话,就只能通过遍历 oldCh 中剩余得节点,一一进行匹配获取对应下标 index ,这个也可以封装成一个函数去实现。

js 复制代码
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
    }
}

接下来继续写代码

js 复制代码
let oldKeyToIdx, idxInOld;

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldEndVnode, newStartVnode)){
            ...
      }else{
        		// 遍历剩余的旧孩子节点,将有key值的生成index表 <{key: i}>
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            // 如果newStartVnode存在key,就进行匹配index值;如果没有key值,遍历剩余的旧孩子节点,一一与newStartVnode匹配,相同节点的返回index
            idxInOld = isDef(newStartVnode.key)
                ? oldKeyToIdx[newStartVnode.key]
                : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      }
}

当然这种情况下,idxInOld 下标值还是有可能为空,这种情况就代表那个 newStartVnode 是一个全新的节点,这时候我们只需要新增节点就可以了

如果 idxInOld 不为空的话,我们就获取对应的oldVnode ,然后与 newStartVnode 进行比较,如果是相同结点的话,调用 patchVnode() 函数,并且将对应的 oldVnode 设置为 undefined ;如果匹配出来是不同节点,那就直接创建一个节点即可。

最后,移动一下 newStatrIdx

js 复制代码
let oldKeyToIdx, idxInOld, vnodeToMove;

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldEndVnode, newStartVnode)){
            ...
      }else{
        		// 遍历剩余的旧孩子节点,将有key值的生成index表 <{key: i}>
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            // 如果newStartVnode存在key,就进行匹配index值;如果没有key值,遍历剩余的旧孩子节点,一一与newStartVnode匹配,相同节点的返回index
            idxInOld = isDef(newStartVnode.key)
                ? oldKeyToIdx[newStartVnode.key]
                : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
            if (isUndef(idxInOld)) { 
                // 如果匹配不到index,则创建新节点
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
                // 获取对应的旧孩子节点
                vnodeToMove = oldCh[idxInOld]
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                    // 因为idxInOld是处于oldStartIdx和oldEndIdx之间,因此只能将其设置为undefined,而不是移动两个指针
                    oldCh[idxInOld] = undefined
                    nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
                } else {
                    // 如果key相同但节点不同,就创建一个新的节点
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
                }
            }
            // 移动新节点的左边指针
            newStartVnode = newCh[++newStartIdx]
      }
}

这里有个重点,如果我们匹配到对应的 oldVnode 的话,需要将其设置为 undefined ,同时当后面我们的oldStartIdx 和 oldEndIdx 移动后,如果判断出对应的 Vnode 为 undefined 时,就需要选择跳过。

js 复制代码
let oldKeyToIdx, idxInOld, vnodeToMove;

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        		// 当oldStartVnode为undefined的时候,oldStartVnode右移
        		oldStartVnode = oldCh[++oldStartIdx] 
      } else if (isUndef(oldEndVnode)) {
        		// 当oldEndVnode为undefined的时候,oldEndVnode左移
        		oldEndVnode = oldCh[--oldEndIdx]
      } else if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldEndVnode, newStartVnode)){
            ...
      }else{
        		...
      }
}

这时候,我们 oldCh 和 newCh 两个数组一一比较差不多了,

如果这个时候,oldCh 的两个指针已经重叠并越过,而 newCh 的两个指针还未重叠;说明 newCh 有多余的 vnode ,我们只需要新增他们就可以了

或者相反情况下(newCh 的两个指针已经重叠并越过,而 oldCh 的两个指针还未重叠)说明 oldCh 有多余的 vnode ,我们只需要删除他们即可。

js 复制代码
let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        		...
      } else if (isUndef(oldEndVnode)) {
        		...
      } else if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldEndVnode, newStartVnode)){
            ...
      }else{
        		...
      }
            
      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)
   		}
}

此时我们就完成了 undateChildren() 方法了,整体代码如下:

js 复制代码
// 比较两组孩子节点
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 设置首尾4个指针和对应节点
    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]

    // diff查找是所需的变量
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // 循环结束条件:新旧节点的头尾指针都重合
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            // 当oldStartVnode为undefined的时候,oldStartVnode右移
            oldStartVnode = oldCh[++oldStartIdx] 
        } else if (isUndef(oldEndVnode)) {
            // 当oldEndVnode为undefined的时候,oldEndVnode左移
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            // 当oldStartVnode与newStartVnode节点相同,对比节点
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 对应两个指针更新
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            // 当oldEndVnode与newEndVnode节点相同,对比节点
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            // 对应两个指针更新
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            // 当oldStartVnode与newEndVnode节点相同,对比节点
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            // 将oldStartVnode节点移动到对应位置,即oldEndVnode节点的后面
            nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            // 对应两个指针更新
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            // 当oldEndVnode与newStartVnode节点相同,对比节点
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 将oldEndVnode节点移动到对应位置,即oldStartVnode节点的前面
            nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            // 对应两个指针更新
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {   // 暴力解法 使用key匹配
            // 遍历剩余的旧孩子节点,将有key值的生成index表 <{key: i}>
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            // 如果newStartVnode存在key,就进行匹配index值;如果没有key值,遍历剩余的旧孩子节点,一一与newStartVnode匹配,相同节点的返回index
            idxInOld = isDef(newStartVnode.key)
                ? oldKeyToIdx[newStartVnode.key]
                : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

            if (isUndef(idxInOld)) { 
                // 如果匹配不到index,则创建新节点
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
                // 获取对应的旧孩子节点
                vnodeToMove = oldCh[idxInOld]
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                    // 因为idxInOld是处于oldStartIdx和oldEndIdx之间,因此只能将其设置为undefined,而不是移动两个指针
                    oldCh[idxInOld] = undefined
                    nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
                } else {
                    // 如果key相同但节点不同,就创建一个新的节点
                    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)
    }
}

参考大佬:juejin.cn/post/697162...

相关推荐
zhaoyang030129 分钟前
css3笔记 (1) 自用
前端·javascript·css·vue.js·笔记·html·css3
珎珎啊34 分钟前
CSS3 常用功能详细使用指南
前端·css·css3
moxiaoran57534 小时前
uni-app萌宠案例学习笔记--页面布局和CSS样式设置
前端·css·uni-app
CrissChan4 小时前
Pycharm 函数注释
java·前端·pycharm
小小小小宇5 小时前
Vue.nextTick()笔记
前端
小约翰仓鼠6 小时前
vue3子组件获取并修改父组件的值
前端·javascript·vue.js
Lin Hsüeh-ch'in6 小时前
Vue 学习路线图(从零到实战)
前端·vue.js·学习
烛阴7 小时前
bignumber.js深度解析:驾驭任意精度计算的终极武器
前端·javascript·后端
计蒙不吃鱼7 小时前
一篇文章实现Android图片拼接并保存至相册
android·java·前端