前言

Vue 源码中虚构 DOM 与 Diff 算法的实现借鉴了 snabbdom 这个库,snabbdom 是一个虚构 DOM 库,它专一于简略,模块化,弱小的性能和性能。要彻底明确虚构 DOM 与 Diff 算法就得剖析 snabbdom 这个库到底做了什么?

获取源代码

能够通过npm i snabbdom -D 来下载 snabbdom 库,这样咱们既能看到 src 下用 Typescript 编写的源码,也能看到编译好的 JavaScript 代码。上面贴的源码是 2.1.0 版本,当初曾经更新到 3.0.3 版本了。倡议将下方呈现的源码复制到下载的 snabbdom 库中相应地位,这样看源码比拟清晰。那咱们就开始剖析源码吧。

源码剖析

JavaScript 对象模仿实在 DOM 树

通过调用 snabbdom 库中的 h函数就能够对实在 DOM 节点进行形象。咱们先来看看一个残缺的虚构 DOM 节点(vnode)是什么样的:

{  sel: "div", // 以后vnode的选择器  elm: undefined, // 以后vnode对应实在的DOM节点  key: undefined, // 以后vnode的惟一标识  data: {}, // 以后vnode的属性,款式等  children: undefined, // 以后vnode的子元素  text: '文本内容' // 以后vnode的文本节点内容}

实际上,h函数的作用就是用 JavaScript 对象模仿实在的 DOM 树,对实在 DOM 树进行形象。调用 h函数就能失去由 vnode 组成的虚构 DOM 树。

调用 h函数有多种形式:

 ① h('div') ② h('div', 'text') ③ h('div', h('p')) ④ h('div', []) ⑤ h('div', {}) ⑥ h('div', {}, 'text') ⑦ h('div', {}, h('span')) ⑧ h('div', {}, [])

使得 h函数的第二和第三个参数比拟灵便,要判断的状况也比拟多,上面把这部分的外围源码剖析贴一下:

// h函数:依据传入的参数揣测出h函数的调用模式以及每个vnode对应属性的属性值export function h(sel: string): VNodeexport function h(sel: string, data: VNodeData | null): VNodeexport function h(sel: string, children: VNodeChildren): VNodeexport function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNodeexport function h(sel: any, b?: any, c?: any): VNode {  var data: VNodeData = {};  var children: any;  var text: any;  var i: number  // c有值,状况有:⑥ ⑦ ⑧  if (c !== undefined) {     // c有值的状况下b有值,状况有:⑥ ⑦ ⑧    if (b !== null) {       // 将b赋值给data       data = b      }    // c的数据类型是数组,状况有:⑧    if (is.array(c)) {       children = c     // 判断c是文本节点,状况有:⑥    } else if (is.primitive(c)) {       text = c     // 状况有:⑦,⑦这条语句会先执行h('span')代码,间接调用vnode函数,调用后会返回{sel: 'span'},    // 这时c有值并且c并且含有sel属性    } else if (c && c.sel) {      // 注:这里的c不是h('span'),而是h('span')的返回值,是个{ sel: 'span' }这样的对象,      // 最初组装成数组赋值给children      children = [c]    }  // c没有值,b有值,状况有:② ③ ④ ⑤  } else if (b !== undefined && b !== null) {     // b的数据类型是数组,状况有:④    if (is.array(b)) {       children = b     // 判断b是文本节点,状况有:②    } else if (is.primitive(b)) {       text = b     // 状况有:③,③这条语句会先执行h('p')代码,间接调用vnode函数,调用后会返回{sel: 'p'},    // 这时b有值并且b并且含有sel属性    } else if (b && b.sel) {      // 注:这里的b不是h('p'),而是h('p')的返回值,是个{ sel: 'p' }这样的对象,      // 最初组装成数组赋值给children      children = [b]     // 状况有:⑤,将b赋值给data    } else { data = b }   }  // children有值,遍历children  if (children !== undefined) {     for (i = 0; i < children.length; ++i) {      // 判断children中的每一项的数据类型是否是string/number,调用vnode函数      if (is.primitive(children[i])) {          children[i] = vnode(undefined, undefined, undefined, children[i], undefined)      }    }  }  /**   * 调用vnode后返回形如   * {   *    sel: 'div',   *    data: { style: '#000' },   *    children: undefined,   *    text: 'text',   *    elm: undefined,    *    key: undefined   * }   * 这样的JavaScript对象  */  return vnode(sel, data, children, text, undefined);  }
// vnode函数:依据传入的参数组装vnode构造export function vnode(sel: string | undefined,  data: any | undefined,  children: Array<VNode | string> | undefined,  text: string | undefined,  elm: Element | Text | undefined): VNode {  // 判断data是否有值,有值就将data.key赋值给key,无值就将undefined赋值给key  const key = data === undefined ? undefined : data.key   // 将传入vnode函数的参数组装成一个对象返回  return { sel, data, children, text, elm, key } }

diff 算法--入口函数

通过 h函数失去新旧虚构节点 DOM 对象后就能够进行差别比拟了。在理论应用过程中,咱们是间接调用 snabbdompatch 函数,而后传入两个参数,通过 patch 函数外部解决就能够失去新旧虚构节点 DOM 对象的差别,并将差别局部更新到真正的 DOM 树上。

首先,patch 函数会去判断 oldVnode 是否是实在DOM节点,如果是则须要先转换为虚构DOM节点 oldVnode = emptyNodeAt(oldVnode) ;而后去比拟新旧 vnode 是否是同一个节点 sameVnode(oldVnode, vnode),如果是同一节点则准确比拟新旧 vnode patchVnode(oldVnode, vnode, insertedVnodeQueue) ,如果不是则间接创立新 vnode 对应的实在 DOM 节点 createElm(vnode, insertedVnodeQueue),在 createElm 函数中创立新 vnode 的实在 DOM 节点以及它对应的子节点,并把子节点插入到相应地位。如果 oldVnode.elm 有父节点则把新 vnode 对应的实在 DOM 节点作为子节点插入到相应地位,并且删除旧节点。上面贴一下 patch 函数的源码解析:

function patch(oldVnode: VNode | Element, vnode: VNode): VNode {    let i: number, elm: Node, parent: Node    const insertedVnodeQueue: VNodeQueue = []    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()    // isVnode(oldVnode)判断oldVnode.sel是否存在,不存在示意oldVnode是实在的DOM节点    if (!isVnode(oldVnode)) {      // oldVnode可能是实在的DOM节点,也可能是旧的虚构DOM节点,      // 如果是实在的DOM节点要调用vnode函数组装成虚构DOM节点      oldVnode = emptyNodeAt(oldVnode)    }    // 判断出是同一个虚构DOM节点    if (sameVnode(oldVnode, vnode)) {       // 准确比拟两个虚构DOM节点      patchVnode(oldVnode, vnode, insertedVnodeQueue)     } else {      // oldVnode.elm是虚构DOM节点对应的实在DOM节点      elm = oldVnode.elm!       // api.parentNode(elm)获取elm的父节点elm.parentNode      parent = api.parentNode(elm) as Node       // 创立vnode下实在DOM节点并更新到相应地位      createElm(vnode, insertedVnodeQueue)       // elm的父节点存在      if (parent !== null) {         // api.nextSibling(elm)-->elm.nextSibling 返回紧跟在elm之后的节点        // api.insertBefore(parent, B, C)-->-->parent.insertBefore(B, C),将B节点插入到C节点之前        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))        removeVnodes(parent, [oldVnode], 0, 0) // 删除旧的DOM节点      }    }    for (i = 0; i < insertedVnodeQueue.length; ++i) {      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])    }    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()    return vnode  }

patch 函数中用到了 emptyNodeAt 函数,这个函数次要是解决 patch 函数第一个参数为实在DOM节点的状况。上面贴一下这个函数的源码解析:

  function emptyNodeAt(elm: Element) {    // 判断传入的DOM节点elm有没有id属性,因为虚构DOM节点的sel属性是选择器,例如:div#wrap    const id = elm.id ? '#' + elm.id : ''     // 判断传入的ODM节点elm有没有class属性,因为虚构DOM节点的sel属性是选择器,例如:div.wrap    const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''     // 调用vnode函数将传入的DOM节点组装成虚构DOM节点    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)   }

patch 函数中用到了 sameVnode 函数,这个函数次要用来比拟两个虚构DOM节点是否是同一个虚构节点。上面贴一下这个函数的源码剖析:

function sameVnode(vnode1: VNode, vnode2: VNode): boolean {  // 判断vnode1和vnode2是否是同一个虚构DOM节点  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel }

diff 算法--新旧 vnode 不是同一个节点的状况

依据 sameVnode 函数返回的后果,新旧 vnode 不是同一个虚构节点。首先获取到 oldVnode 对应实在 DOM 节点的父节点 parent ,而后调用 createElm 函数去创立 vnode 对应的实在 DOM 节点以及它的子节点和标签属性等等。判断是否有 parent, 如果有则将 vnode.elm 对应的DOM节点作为子节点插入到 parent 节点下的相应地位。局部源码剖析在patch函数中,上面贴一下 createElm 函数的源码剖析:

 function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {    let i: any    let data = vnode.data    if (data !== undefined) {      const init = data.hook?.init      if (isDef(init)) {        init(vnode)        data = vnode.data      }    }    const children = vnode.children    const sel = vnode.sel    // 判断sel值中是否蕴含!    if (sel === '!') {      if (isUndef(vnode.text)) {        vnode.text = ''      }      // --> document.createComment(vnode.text!)创立正文节点      vnode.elm = api.createComment(vnode.text!)    } else if (sel !== undefined) {      // 解析sel选择器      // 查找sel属性值中#的索引,没找到返回-1      const hashIdx = sel.indexOf('#')      // hashIdx作为起始地位查找sel属性值中.的索引,如果hashIdx < 0 那么从地位0开始查找      const dotIdx = sel.indexOf('.', hashIdx)      const hash = hashIdx > 0 ? hashIdx : sel.length      const dot = dotIdx > 0 ? dotIdx : sel.length      // 若id选择器或class选择器存在,则从0位开始截取到最小索引值的地位完结,截取出的就是标签名称      // 都不存在间接取sel值      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel      // 依据tag名创立DOM元素      const elm = vnode.elm = isDef(data) && isDef(i = data.ns)        ? api.createElementNS(i, tag)        : api.createElement(tag)      // 设置id属性      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))      // 设置calss属性      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)      // 判断children是否是数组,是数组则遍历children      if (is.array(children)) {        for (i = 0; i < children.length; ++i) {          const ch = children[i]          if (ch != null) {            // createElm(ch as VNode, insertedVnodeQueue)递归创立子节点            // api.appendChild(A, B)-->A.appendChild(B)将B节点插入到指定父节点A的子节点列表的开端            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))          }        }        // 判断vnode.text有没有值      } else if (is.primitive(vnode.text)) {        // api.createTextNode(vnode.text)依据vnode.text创立文本节点        // api.appendChild(elm, B)-->A.appendChild(B)将文本节点B增加到父节点elm子节点列表的开端处        api.appendChild(elm, api.createTextNode(vnode.text))      }      const hook = vnode.data!.hook      if (isDef(hook)) {        hook.create?.(emptyNode, vnode)        if (hook.insert) {          insertedVnodeQueue.push(vnode)        }      }    } else {      // sel不存在间接创立文本节点      vnode.elm = api.createTextNode(vnode.text!)    }    return vnode.elm  }

diff 算法--新旧 vnode 是同一个节点的状况

下面剖析了新旧 vnode 不是同一个虚构节点,那么是同一个虚构节点又该怎么去解决?首先,调用 patchVnode 函数 patchVnode(oldVnode, vnode, insertedVnodeQueue),这个函数会对新旧 vnode 进行准确比拟:

① 如果新旧虚构 DOM 对象全等 oldVnode === vnode ,那么不做任何操作,间接返回;

② 而后判断 vnode 是否有文本节点 isUndef(vnode.text) ,如果没有文本节点则判断 oldVnode 与 vnode 有没有子节点 isDef(oldCh) && isDef(ch),如果都有子节点且不相等则调用 updateChildren 函数去更新子节点;

③ 如果只有 vnode 有子节点而 oldVnode 有文本节点或没有内容,将 oldVnode 的文本节点置空或不做解决,调用 addVnodes 函数将 vnode 的子节点创立出对应的实在DOM并循环插入到父节点下;

④ 如果只有 oldVnode 有子节点而 vnode 没有内容,则间接删除 oldVnode 下的子节点;

⑤ 如果只有 oldVnode 有文本节点而 vnode 没有内容,则将 oldVnode 对应的实在 DOM 节点的文本置空;

⑥ 如果 vnode 有文本节点,oldVnode 有子节点就将对应实在 DOM 节点的子节点删除,没有就不解决,而后将 vnode 的文本节点作为子节点插入到对应实在 DOM 节点下。

局部源码剖析在patch函数中,上面贴一下 patchVnode 函数的源码剖析:

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {    const hook = vnode.data?.hook    hook?.prepatch?.(oldVnode, vnode)    const elm = vnode.elm = oldVnode.elm!    const oldCh = oldVnode.children as VNode[]    const ch = vnode.children as VNode[]    // oldVnode与vnode齐全相等并没有须要更新的内容则间接返回,不做解决    if (oldVnode === vnode) return     if (vnode.data !== undefined) {      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)      vnode.data.hook?.update?.(oldVnode, vnode)    }    // vnode.text为undefined示意vnode虚构节点没有文本内容    if (isUndef(vnode.text)) {       // oldCh与ch都不为undefined示意oldVnode与vnode都有虚构子节点children      if (isDef(oldCh) && isDef(ch)) {         // oldCh !== ch 利用算法去更新子节点        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)      } else if (isDef(ch)) {         // 将oldVnode的文本节点设置为''        if (isDef(oldVnode.text)) api.setTextContent(elm, '')         // 调用addVnodes办法将vnode的虚构子节点循环插入到elm节点的子列表下        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)      // oldCh不为undefined示意oldVnode有虚构子节点children      } else if (isDef(oldCh)) {         // vnode没有children则间接删除oldVnode的children        removeVnodes(elm, oldCh, 0, oldCh.length - 1)       // oldVnode.text有值而vnode.text没有值      } else if (isDef(oldVnode.text)) {         // 将oldVnode的文本节点设置为''        api.setTextContent(elm, '')       }    // oldVnode与vnode文本节点内容不同    } else if (oldVnode.text !== vnode.text) {       // isDef(oldCh)-->oldCh !== undefined 表明oldVnode虚构节点下有虚构子节点      if (isDef(oldCh)) {         removeVnodes(elm, oldCh, 0, oldCh.length - 1)      }      // oldCh虚构节点下没有虚构子节点则间接更新文本内容      api.setTextContent(elm, vnode.text!)    }    hook?.postpatch?.(oldVnode, vnode)  }

diff 算法--新旧 vnode 子节点的更新策略

当新旧 vnode 都有子节点时,diff 算法定义了四个指针来解决子节点,四个指针别离是:oldStartVnode(旧前vnode)/newStartVnode(新前vnode)/oldEndVnode(旧后vnode)/newEndVnode(新后vnode) 。进入循环体内后,新旧 vnode 的子节点两两比拟,这里提供了一套比拟规定,如下图:

如果下面四个规定都不满足,将 oldVnode 的子节点从旧的前索引 oldStartIdx 到旧的后索引 oldEndIdx 做一个 key 与对应地位序号的映射 oldKeyToIdx ,通过新 vnode 的 key 去找 oldKeyToIdx 中是否有对应的索引值,若没有,表明 oldVnode 没有对应的旧节点,是一个新增的节点,进行插入操作;若有,表明 oldVnode 有对应的旧节点,不是一个新增节点,进行挪动操作。上面贴一下源码解析:

// 旧vnode的子节点的前索引oldStartIdx到后索引oldEndIdx的key与对应地位序号的映射关系function createKeyToOldIdx(children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {  const map: KeyToIndexMap = {}  for (let i = beginIdx; i <= endIdx; ++i) {    const key = children[i]?.key    if (key !== undefined) {      map[key] = i    }  }  /**   * 例如:map = { A: 1, B: 2 }  */  return map}
function updateChildren(parentElm: Node,    oldCh: VNode[],    newCh: VNode[],    insertedVnodeQueue: VNodeQueue) {    let oldStartIdx = 0 // 旧的前索引    let newStartIdx = 0 // 新的前索引    let oldEndIdx = oldCh.length - 1 // 旧的后索引    let newEndIdx = newCh.length - 1 // 新的后索引    let oldStartVnode = oldCh[0] // 旧的前vnode    let newStartVnode = newCh[0] // 新的前vnode    let oldEndVnode = oldCh[oldEndIdx] // 旧的后vnode    let newEndVnode = newCh[newEndIdx] // 新的后vnode    let oldKeyToIdx: KeyToIndexMap | undefined    let idxInOld: number    let elmToMove: VNode    let before: any    // 当旧的前索引 <= 旧的后索引 && 新的前索引 <= 新的后索引时执行循环语句    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {      // 为什么oldStartVnode == null?       // 因为虚构节点进行挪动操作后要将原来的虚构节点置为undefined了      // oldCh[idxInOld] = undefined as any      if (oldStartVnode == null) {        // oldStartVnode为null就过滤掉以后节点,取oldCh[++oldStartIdx]节点(旧的前索引的下一个索引的节点)        oldStartVnode = oldCh[++oldStartIdx]      } else if (oldEndVnode == null) {        // oldEndVnode为null就过滤掉以后节点,取oldCh[--oldEndIdx]节点(旧的后索引的上一个索引的节点)        oldEndVnode = oldCh[--oldEndIdx]      } else if (newStartVnode == null) {        // newStartVnode为null就过滤掉以后节点,取newCh[++newStartIdx]节点(新的前索引的下一个索引的节点)        newStartVnode = newCh[++newStartIdx]      } else if (newEndVnode == null) {        // newEndVnode为null就过滤掉以后节点,取newCh[--newEndIdx]节点(新的后索引的上一个索引的节点)        newEndVnode = newCh[--newEndIdx]      } else if (sameVnode(oldStartVnode, newStartVnode)) {        /**        * ① 旧的前vnode(oldStartVnode) 与 新的前vnode(newStartVnode) 比拟是否是同一个虚构节点        * 旧的虚构子节点                       新的虚构子节点        * h('li', { key: 'A' }, 'A')      h('li', { key: 'A' }, 'A')        * h('li', { key: 'B' }, 'B')      h('li', { key: 'B' }, 'B')       */        // 如果判断是同一个虚构节点则调用patchVnode函数        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)        // oldCh[++oldStartIdx]取旧的前索引节点的下一个虚构节点(例子中key为B的节点),赋值给oldStartVnode        oldStartVnode = oldCh[++oldStartIdx]        // oldCh[++oldStartIdx]取新的前索引节点的下一个虚构节点(例子中key为B的节点),赋值给newStartVnode        newStartVnode = newCh[++newStartIdx]      } else if (sameVnode(oldEndVnode, newEndVnode)) {        /**         * 如果旧的前vnode(例子中key为B的虚构节点) 与 新的前vnode(例子中key为B的虚构节点)          * 不是同一个虚构节点则进行计划②比拟         * ② 旧的后vnode(oldEndVnode) 与 新的后vnode(newEndVnode) 比拟是否是同一个虚构节点         * 旧的虚构子节点                   新的虚构子节点         * h('li', { key: 'C' }, 'C')      h('li', { key: 'A' }, 'A')         * h('li', { key: 'B' }, 'B')      h('li', { key: 'B' }, 'B')        */        // 如果判断是同一个虚构节点则调用patchVnode函数        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)        // oldCh[--oldEndIdx]取旧的后索引节点的上一个虚构节点(例子中key为C的虚构节点),赋值给oldEndVnode        oldEndVnode = oldCh[--oldEndIdx]        // newCh[--newEndIdx]取新的后索引节点的上一个虚构节点(例子中key为A的虚构节点),赋值给newEndVnode        newEndVnode = newCh[--newEndIdx]      } else if (sameVnode(oldStartVnode, newEndVnode)) {        /**        * 如果旧的后vnode 与 新的后vnode 不是同一个虚构节点则进行计划③比拟        * ③ 旧的前vnode(oldStartVnode) 与 新的后vnode(newEndVnode) 比拟是否是同一个虚构节点        * 旧的虚构子节点                   新的虚构子节点        * h('li', { key: 'C' }, 'C')      h('li', { key: 'A' }, 'A')        * h('li', { key: 'B' }, 'B')      h('li', { key: 'B' }, 'B')        *                                 h('li', { key: 'C' }, 'C')       */        // 如果判断是同一个虚构节点则调用patchVnode函数        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)        // 将旧的前vnode(相当于例子中key为C的虚构节点)插入到以后旧的后vnode的下一个兄弟节点的后面        // 如果oldEndVnode是最开端的虚构节点,则node.nextSibling会返回null,        // 则新的虚构节点直接插入到最开端,等同于appenChild        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))        // oldCh[++oldStartIdx]取旧的前索引虚构节点的下一个虚构节点(例子中key为B的虚构节点),赋值给oldStartVnode        oldStartVnode = oldCh[++oldStartIdx]        // newCh[--newEndIdx]取新的后索引虚构节点的上一个虚构节点(例子中key为B的虚构节点),赋值给newEndVnode        newEndVnode = newCh[--newEndIdx]      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left        /**        * 如果旧的前vnode 与 新的后vnode 不是同一个虚构节点则进行计划④比拟        * ④ 旧的后vnode(oldEndVnode) 与 新的前vnode(newStartVnode) 比拟是否是同一个虚构节点        * 旧的虚构子节点                   新的虚构子节点        * h('li', { key: 'C' }, 'C')      h('li', { key: 'B' }, 'B')        * h('li', { key: 'B' }, 'B')      h('li', { key: 'A' }, 'A')        *                                 h('li', { key: 'C' }, 'C')       */        // 如果判断是同一个虚构节点则调用patchVnode函数        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)        // 将旧的后vnode(例子中key为B)插入到以后旧的前vnode(例子中key为C)的后面        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)        // oldCh[--oldEndIdx]取旧的后索引节点的上一个虚构节点(例子中key为C的虚构节点),赋值给oldEndVnode        oldEndVnode = oldCh[--oldEndIdx]        // newCh[++newStartIdx]取新的前索引节点的下一个虚构节点(例子中key为A的虚构节点),赋值给newStartVnode        newStartVnode = newCh[++newStartIdx]      } else {        // 不满足以上四种状况        if (oldKeyToIdx === undefined) {          // oldKeyToIdx保留旧的children中各个节点的key与对应地位序号的映射关系          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)        }        // 从oldKeyToIdx中获取以后newStartVnode节点key对应的序号        idxInOld = oldKeyToIdx[newStartVnode.key as string]        if (isUndef(idxInOld)) { // isUndef(idxInOld) --> idxInOld === undefined          /**           * idxInOld = undefined 要插入节点           * 旧的虚构子节点中没有idxInOld对应的节点,而新的虚构子节点中有,           * 所以newStartVnode是须要插入的虚构节点           * 旧的虚构子节点                   新的虚构子节点           * h('li', { key: 'A' }, 'A')      h('li', { key: 'C' }, 'C')            * h('li', { key: 'B' }, 'B')          */          // 依据newStartVnode(例子中key为C的虚构节点)创立实在DOM节点createElm(),          // 将创立的DOM节点插入到oldStartVnode.elm(例子中key为A的节点)的后面          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)        } else {          /**           * idxInOld != undefined 要挪动节点           * 旧的虚构子节点中有idxInOld对应的节点,所以oldCh[idxInOld]是须要挪动的虚构节点           * 旧的虚构子节点                   新的虚构子节点           * h('li', { key: 'A' }, 'A')      h('div', { key: 'B' }, 'B')           * h('li', { key: 'B' }, 'B')      h('li', { key: 'D' }, 'D')                                                                */          elmToMove = oldCh[idxInOld] // elmToMove保留要挪动的虚构节点          // 判断elmToMove与newStartVnode在key雷同的状况下sel属性是否雷同          if (elmToMove.sel !== newStartVnode.sel) {            // sel属性不雷同表明不是同一个虚构节点,            // 依据newStartVnode虚构节点创立实在DOM节点并插入到oldStartVnode.elm(旧的key为A的节点)之前            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)          } else {            // key与sel雷同示意是同一个虚构节点,调用patchVnode函数            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)            // 解决完被挪动的虚构节点oldCh[idxInOld]要设置为undefined,不便下次循环解决时过滤掉曾经解决的节点            oldCh[idxInOld] = undefined as any            // 将elmToMove.elm(例子中旧的key为B的节点)插入到oldStartVnode.elm(例子中key为A的节点)的后面            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)          }        }        // 取newCh[++newStartIdx]虚构节点(例子中key为D的虚构节点)赋值给newStartVnode        newStartVnode = newCh[++newStartIdx]      }    }    /**     * 循环完结后旧的前索引 <= 旧的后索引 || 新的前索引 <= 新的后索引,     * 示意还有局部虚构节点(例子中key为C的虚构节点)没解决     * 旧的虚构子节点                   新的虚构子节点     * 状况一:     * h('li', { key: 'A' }, 'A')      h('li', { key: 'A' }, 'A')     * h('li', { key: 'B' }, 'B')      h('li', { key: 'B' }, 'B')     * h('li', { key: 'D' }, 'D')      h('li', { key: 'C' }, 'C')     *                                 h('li', { key: 'D' }, 'D')     * 状况二:     * h('li', { key: 'A' }, 'A')      h('li', { key: 'A' }, 'A')     * h('li', { key: 'B' }, 'B')      h('li', { key: 'B' }, 'B')     * h('li', { key: 'C' }, 'C')    */    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {      // 解决例子中状况一      if (oldStartIdx > oldEndIdx) {        // 待插入的节点以before节点为参照,newCh[newEndIdx]是例子中新的子节点中key为C的虚构节点,        // 所以before = newCh[newEndIdx + 1]是key为D的虚构节点        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm        // 例子中当初newStartIdx,newEndIdx都为2        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)      } else {        // 解决例子中状况二,删除旧的前索引到旧的后索引两头的节点(例子中删除旧的key为C的虚构节点)        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)      }    }  }