Vue2的diff算法--updateChildren图文流程以及缺点

906 阅读9分钟

Vue2的children-diff

Vue框架中update的流程中,主要是对比两棵子树,遵循同级比较、深度优先的规则

  • 深度优先是指一棵树的先序遍历的顺序来遍历。

  • 同级比较是指比较两组同深度的Vnode。

本篇文章讲述比较两组同深度的Vnode的节点的原理。

(第一次发掘金文章,如有错误欢迎指出,也希望有大佬能指点或者纠错,谢谢~)

什么时候Vnode会进行patch?

什么情况下才会进行patch更新,当两个VnodesameVnode的时候,才会进行patch更新

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)
      )
    )
  )
}
// 这个函数我理解的并不深,我只能理解到tag相同,key相同的程度,另外可能有ssr的水合注入的情况,理解不全

两个节点是sameVnode,那么就会执行patchVnode,去更新这两个节点

两个Vnode是sameVnode,怎么更新?(只关注children的更新)

props,events,attrs,domProps等等更新这篇文章不是重点

我们只关注children的更新

  • oldChildren是空

    • newChildren是空,不用变
    • newChildren是文本,添加文本节点
    • newChildren是数组,添加多个子节点
  • oldChildren是text

    • newChildren是空,清空文本节点
    • newChildren是文本,设置文本内容进行替换
    • newChildren是数组,清空文本节点,添加多个子节点
  • oldChildren是数组

    • newChildren是空,清空多个子节点
    • newChildren是文本,清空多个子节点,添加文本节点
    • newChildren是数组,完整diff子节点(updateChildren,本文主要讲述的原理)
Vue2中新旧子节点数组进行对比,遵循的规则?
  1. 旧的头与新的头进行对比
  2. 旧的尾与新的尾进行对比
  3. 旧的头与新的尾进行对比
  4. 旧的尾与新的头进行对比
  5. 未知序列的查找与对比
基于以上原则,Vue2中使用双指针的方式来完成以上的操作
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // 旧children的头部指针
    let newStartIdx = 0 // 新children的头部指针
    let oldEndIdx = oldCh.length - 1 // 旧children的尾部指针
    let oldStartVnode = oldCh[0] // 旧children的头部Vnode
    let oldEndVnode = oldCh[oldEndIdx] // 旧children的尾部Vnode
    let newEndIdx = newCh.length - 1 // 新children的尾部指针
    let newStartVnode = newCh[0] // 新children的头部Vnode
    let newEndVnode = newCh[newEndIdx] // 新children的尾部Vnode
    
    
    // diff过程中需要用到的变量,后面都会说到
    let oldKeyToIdx // 遍历旧children的oldStartIdx到oldEndIdx区间内的key --> oldIdx的映射Map
    let idxInOld // 当前操作的新Vnode在旧children的oldStartIdx到oldEndIdx区间内的oldIdx
    let vnodeToMove // 需要移动的旧节点
    let refElm // 是一个锚点
}

图1:

vue2-diff-keyedChildren.png

updateChildren
头部对比

先从旧children和新children的头部进行对比

  • 如果是sameVnode

    • 进行patchVnode更新
    • 同时oldstartIndexnewStartIndex各自右移一位
  • 如果不是sameVnode,则进入下一步条件

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // ...
    
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
     if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      }
    }
}

图2:

vue2-diff-keyedChildren-1.jpg

尾部对比

从旧children和新children的尾部进行对比

  • 如果是sameVnode

    • 进行patchVnode更新
    • oldEndIndexnewEndIndex左移一位
  • 如果不是sameVnode,则进入下一步条件
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // ...
    
    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]
      }
    }
}

图3:

vue2-diff-keyedChildren-2.jpg

旧头新尾对比

从旧children的头部和新children的尾部进行对比

  • 如果是sameVnode

    • 进行patchVnode更新
    • oldEndVnode.elm.nextSibling为锚点,将oldStartVnode.elm插入到oldEndVnode.elm.nextSibling之前(因为这个时候对比的是新children的尾部,所以如果此时这个条件,旧children的头部可以移动到上个确定的旧尾节点之前,那从上图就可以看出来,{key = 9}的旧尾节点是上个确定的旧尾节点,从已有的变量中,可以得到是oldEndVnode的下一个相邻节点)
    • 同时newEndIndex左移一位,oldStartIndex右移一位
  • 如果不是sameVnode,则进入下一步条件
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // ...
    
    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)
        // 因为oldEndVnode在第二步条件(尾部对比中没有比对成功,就说明oldEndVnode的下一个节点是对比成功的,那么锚点就应该是oldEndVnode.elm.nextsibling,但是其实也有可能没有下一个节点,那么就置为null即可)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // 指针更新
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      }
    }
}

图4:

vue2-diff-keyedChildren-3.jpg

旧尾新头对比

对旧children的尾部和新children的头部进行对比

  • 如果是sameVnode

    • 进行patchVnode更新
    • oldStartVnode.elm为锚点,将oldEndVnode.elm插入到oldStartVnode.elm之前(因为此时对比的是新children的头部,所以对比成功时,说明oldEndVnode.elm可以复用,那么可以将其插入到当前旧children头部已经确定对比过的最右面一个节点之后,图中看就是要插入到oldStartVnode.elm之前)
    • oldEndIndex左移一位,newStartIndex右移一位
  • 如果不是sameVnode,进入下一步条件
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // ...
    
    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)
        // 我觉得这两步头尾,尾头对比主要是锚点的问题难理解,其实可以多画图和画指针位置就能理解了
        // 我可能上述解释的不太好,不过我加上了图,希望能帮助理解吧
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      }
    }
}

图5:

vue2-diff-keyedChildren-4.jpg

未知序列对比

由于前四步的对比都无法成立,接下来就是未知序列的处理,首先大体说下原理吧

  1. 我们可以想到的是,我需要尽最大力来完成旧节点的复用,所以我们遍历当前的newStartVnode的时候,需要看在oldChildren中的[oldStartIndex, oldEndIndex]区间内是否有相同节点可以复用

  2. 具体实现方式,Vue2是这样处理的:对oldChildren中的[oldStartIndex, oldEndIndex]区间内所有旧节点生成key --> oldIndex的映射哈希表,也就是我们最一开始初始化时声明的变量oldKeyToIdx

  3. 当生成oldKeyToIdx之后,就可以开始对当前的newStartVnode进行查找了,而idxInOld这个变量就是每次newStartVnodeoldChildren中的可以复用的旧节点的索引位置

    • 如果idxInOld存在,则说明oldChildren存在可以复用的节点

      • 进行patchVnode更新
      • 重置oldChildren中对应idxInOld的值为undefined
      • 进行移动,将找到的oldVnode.elm移动到oldStartVnode.elm之前(具体原因与第四步中一致,因为oldStartVnode没有被比对,但是它之前的节点已经被比对过了,而第五步中是遍历newStartVnode,也就是newChildren的未知序列头部节点)
    • 如果idxInOld不存在,就说明oldChildren中没有可以复用的节点

      • 新建newStartVnode这个vnode对应的elm,锚点为oldStartVnode.elm
  1. newStartIndex右移一位
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // ...
    
    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 {
        // 判断是否oldKeyToIdx已经计算过了,已经计算过,就不会计算过了(因为在while循环中,代码执行到这个部分可能会有多次,所以只需要计算一次就可以了)
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 查找当前的newStartVnode在oldChildren中的索引
        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)
        // 如果找到了,则进行patch更新、dom移动操作、重置oldChildren中的节点(因为已经diff过的旧节点是不能再进入五个判断条件了)
        } else {
          // 拿到oldChildren中的对应复用节点
          vnodeToMove = oldCh[idxInOld]
          // 如果是相同节点,则patch、移动、重置
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // 如果不是,则创建新的节点,有可能
            // <div key="1"></div> 和 <span key="1"></span>
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        // newStartIdx指针右移一位
        newStartVnode = newCh[++newStartIdx]
      }
    }
}
// 基于上图5的情况,生成的oldKeyToIdx结构为
oldKeyToIdx = {
  [key = 3] : 2,
  [key = 4] : 3,
  [key = 5] : 4,
  [key = 6] : 5,
  [key = 7] : 6
}

当前newStartVnode.key = 5,可以找到对应的oldVnode`,执行第五步的步骤,变为下图6结构

图6:

vue2-diff-keyedChildren-5.jpg

// 这一步完成后的oldChildren,这个重置有什么用呢,是因为未来如果oldStartIndex指针或者oldEndIndex指针访问到这个节点5了,其实这个节点5已经被处理过,应该被跳过,也就是说需要保证指针到节点5的时候,直接跳过
oldChildren = [1, 2, 3, 4, undefined, 6, 7, 8, 9]

下一步

当前newStartVnode.key = 6,可以找到对应的oldVnode,执行第五步的步骤,变为下图7结构

图7:

vue2-diff-keyedChildren-6.jpg

// oldChildren最新状态
oldChildren = [1, 2, 3, 4, undefined, undefined, 7, 8, 9]

下一步

当前newStartVnode.key = 7,但是现在的条件满足旧的尾部和新头部的对比了,所以不会进入到第五步了,而是进入第四步的处理逻辑,变为图8结构:

图8:

vue2-diff-keyedChildren-7.jpg

oldStartIdx和oldEndIdx指向的oldChild已经被diff过的情况

注意,此时oldEndIndex已经指向了已经用过的旧节点了,所以需要跳过

代码

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // ...
    
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // oldStartIdx指针或者oldEndIdx指针所在位置的oldChild被使用过就进行指针移动
      // 如果oldStartVnode被diff过,就oldStartIdx右移一位
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx]
      // 如果oldEndVnode被diff过,就oldEndIdx左移一位
      } else if (isUndef(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 {
        // ...
      }
    }
}

图9:

vue2-diff-keyedChildren-8.jpg

下一步

由于此时的状态是头头对比进行两次第一步的处理即可了,所以这最后一步就显得简单了,所以直接给到最终状态的图了

图10:

vue2-diff-keyedChildren-9.jpg

// 注意此时(oldStartIndex > oldEndIndex) || (newStartIndex > newEndIndex) 
// 所以上述代码中的循环条件就被打断了
// 所以跳出while循环结束这个例子的流程
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // ...
    // (oldStartIndex > oldEndIndex) || (newStartIndex > newEndIndex) 跳出while循环
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // ...
    }
}

以上的例子只是说明了while循环中的流程,还有两种情况我们没有考虑到,接下来看下这两种情况

oldChilren中有部分节点没被diff过(oldStartIndex <= oldEndIndex && newStartIndex > newEndIndex)

先来看图

图11:

vue2-diff-keyedChildren-old1.jpg

如图11所示,此时进行的步骤是连续两次头部对比,到下图12状态

图12:

vue2-diff-keyedChildren-old2.jpg

此时,while循环结束,但是现在条件是

newStartIdx > newEndIndex && oldStartIndex <= oldEndIndex

所以我们还需要将oldChildren中不用的节点给卸载掉

代码:

if (newStartIdx > newEndIndex && oldStartIndex <= oldEndIndex) {
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
newChilren中有部分节点没被diff过(oldStartIndex > oldEndIndex && newStartIndex <= newEndIndex)

老样子,先来看图:

图13:

vue2-diff-keyedChildren-new1.jpg

如图13所示,此时进行的步骤是连续两次头部对比,到下图14状态

图14:

vue2-diff-keyedChildren-new2.jpg

此时,while循环结束,但是现在条件是

newStartIdx <= newEndIndex && oldStartIndex > oldEndIndex

所以我们还需要将newChildren中的未diff的节点给新增上

代码:

if (newStartIdx <= newEndIndex && oldStartIndex > oldEndIndex) {
  // refElm锚点,由上图14所示
  // 如果节点key = 10想要插入到序列中,它相对的锚点是key = 2
  // 那么key = 2是我们可以利用的变量的哪个呢,很明显是newChildren[newEndIndex + 1]
  // 但是我们的例子是新孩子尾部有diff的情况,有可能新孩子的尾部没有一个能diff过
  // 所以需要判断临界状态,在父节点末尾追加
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  // 追加节点,至于addVnodes等等api就不再去讲啦,主要是为了理解这个流程
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}

完整代码:

  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 = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        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)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            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)
    }
  }
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
}
Vue2中updateChildren的缺点

至此,updateChildren的流程就搞完了,但是Vue2中这个流程一样存在性能上的一些缺点。

如果有注意,以上图5、图6、图7、图8、图9都统计了dom节点移动的次数,一共移动了5次。

但是,其实我们来看这个序列

oldChildren : 2 3 4 5 6 7 8
newChildren : 8 5 6 7 3 4 2
// 在Vue3中,尤大的团队采用了移动次数更少的diff算法来解决这个缺点
// 其实更少的移动次数是依据最大递增子序列来处理的
// 以上的oldChildren和newChildren,因为我这个例子是newChildren中的key的顺序就是各自在oldChildren中的相对位置,所以我们可以直接找到最大递增子序列为 5 6 7
// 也就是说5 6 7 这三个节点我们不需要移动
// 只需要将8 移动到 5之前
// 其次将3 4 2依次追加到父节点的末尾即可
// 在Vue3中的diff算法中,这个例子只移动了4次,所以节省了一些dom操作次数,达到了性能优化的目的

Vue3的diff

Vue3的diff也是双指针来处理同级的diff的

但是不同的是,规则是:

  • 旧的头部与新的头部对比

  • 旧的尾部与新的尾部对比

  • 如果oldStartIndex > oldEndIndex && newStartIndex <= newEndIndex,则进行新节点的创建并插入

  • 如果oldStartIndex <= oldEndIndex && newStartIndex > newEndIndex,则进行旧节点的卸载

  • 如果oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex,则进入这一步

    • 先为newChildren创建keyToNewIndexMap,用来判断是否需要移动节点

    • 循环oldChildren(未知子序列区间内)

      • 如果oldChild.key存在于keyToNewIndexMap,就说明有这个节点可以复用,然后与对应的新节点patch更新

        • 这一步还需要看拿到的newIndex是否是递增的,如果不是递增的,就说明是需要移动的,就需要去计算最大递增子序列
        • 以及需要将newChildren对应newIndex位置设置为已经diff过的标志(newIndexToOldIndexMap)(也是为了用来计算最大递增子序列),因为说明已经diff过,就可以判断某个索引位置是否是diff过的,需不需要新建的节点
      • 如果不存在,就卸载掉这个旧节点

    • 如果需要移动,计算最大递增子序列(根据newIndexToOldIndexMap

    • 循环newChildren(未知子序列区间内)

      • 如果newIndexToOldIndexMap中对应索引的位置diff过(条件不赘述了,未diff是0)

        • 如果索引位置在最大递增子序列中,则不动跳过,更新指针
        • 如果索引位置不在最大递增子序列中,则移动,锚点需要注意
      • 如果没被diff过,则新建节点插入
计算最长递增子序列?

用到的算法是二分查找 + 贪心,最好先去leetcode看看如何计算最长递增子序列的长度 leetcode.cn/problems/lo… 具体原理是:以序列中每一项作为递增序列的结尾时,能组成多长的序列以及保存以这一项作为递增序列的结尾时,它相对的递增序列中的前一位索引。因为递增序列满足单调性,所以可以用二分查找去优化时间复杂度。 另外贪心是指,形成的每个长度的递增序列,都会以所能满足该序列的最小值来保存。

结语

第一次写,可能有些地方写的不是很好,也可能没有描述清楚,也可能没有写准确。

如果有错误希望能得到纠正,以及能得到指点。

(Vue3的diff的话,我也想画图输出文章,不过也先看这篇文章的效果吧,毕竟Vue2做不好何以做Vue3呢)