17
头图

Preface

Vue source virtual DOM and achieve Diff algorithm draws snabbdom this library, snabbdom is a virtual DOM library, it focuses on simple, modular, robust functionality and performance. To fully understand the virtual DOM and Diff algorithm, you have to analyze snabbdom library does?

Get the source code

You can download the snabbdom npm i snabbdom -D , so that we can see both src under 060c0a649c7003 and the compiled JavaScript code. The source code posted below is 2.1.0 , which has now been updated to version 3.0.3 It is recommended to copy the source code appearing below to the snabbdom library, so that the source code is clearer. Then let's start analyzing the source code.

Source code analysis

JavaScript objects simulate real DOM trees

The real DOM node can be abstracted by calling h function in the snabbdom Let's first take a look at what a complete virtual DOM node (vnode) looks like:

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

In fact, the h function is to simulate the real DOM tree with JavaScript objects and abstract the real DOM tree. Call h function to get the virtual DOM tree composed of vnode.
image.png
There are many forms of calling the h

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

Makes h function more flexible, and there are more cases to be judged. Here is the core source code analysis of this part:

// h函数:根据传入的参数推测出h函数的调用形式以及每个vnode对应属性的属性值
export function h(sel: string): VNode
export function h(sel: string, data: VNodeData | null): VNode
export function h(sel: string, children: VNodeChildren): VNode
export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export 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 algorithm-entry function

After obtaining the new and old virtual node DOM objects through the h In actual use, we are called directly snabbdom of patch function and pass it two parameters, by patch function internal processing can get the difference between the old and new DOM node virtual object and the real difference partially updated DOM tree.

First, the patch function will determine whether oldVnode is a real DOM node. If it is, it needs to be converted to a virtual DOM node oldVnode = emptyNodeAt(oldVnode) ; then it compares whether the old and new vnode is the same node sameVnode(oldVnode, vnode) . If it is the same node, compare the new and old vnode patchVnode(oldVnode, vnode, insertedVnodeQueue) accurately. createElm(vnode, insertedVnodeQueue) , create the real DOM node 060c0a649c7160 corresponding to the new vnode directly, create the real DOM node of the new vnode and its corresponding child nodes in the createElm function, and insert the child nodes into the corresponding positions. If oldVnode.elm has a parent node, insert the real DOM node corresponding to the new vnode as a child node to the corresponding position, and delete the old node. Post below the source code analysis of 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 function is used in the emptyNodeAt function. This function mainly deals with the situation where the first parameter of the patch The source code analysis of this function is posted below:

  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 function is used in the sameVnode function. This function is mainly used to compare whether two virtual DOM nodes are the same virtual node. The source code analysis of this function is posted below:

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

diff algorithm-when the old and new vnodes are not the same node

According sameVnode , the old and new vnode are not the same virtual node. First obtain the parent node 060c0a649c722d corresponding to the real DOM node of parent , and then call the createElm function to create the real DOM node corresponding to the vnode and its child nodes and label attributes, etc. Determine whether there is parent , and if so, insert the DOM node corresponding to parent as a child node into the corresponding position under the 060c0a649c7231 node. Part of the source code analysis in patch functions, the following stickers createElm source function analysis:

 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 algorithm - the case where the old and new vnodes are the same node

The above analysis has analyzed that the old and new vnodes are not the same virtual node, so how to deal with the same virtual node? First, call the patchVnode function patchVnode(oldVnode, vnode, insertedVnodeQueue) , this function will accurately compare the old and new vnode:

① If the new and old virtual DOM objects are equal to oldVnode === vnode , then do nothing and return directly;

② Then judge whether the vnode has a text node isUndef(vnode.text) , if there is no text node, judge whether oldVnode and vnode have child nodes isDef(oldCh) && isDef(ch) , if there are child nodes and they are not equal, call the updateChildren function to update the child nodes;

③ If only vnode has child nodes and oldVnode has text nodes or no content, leave the text node of oldVnode empty or do nothing, call the addVnodes function to create the corresponding real DOM for the child nodes of vnode and insert it into the parent node in cycles;

④ If only oldVnode has child nodes and vnode has no content, delete the child nodes under oldVnode directly;

⑤ If only oldVnode has a text node and vnode has no content, leave the text of the real DOM node corresponding to oldVnode blank;

⑥ If the vnode has a text node and oldVnode has child nodes, delete the child node corresponding to the real DOM node. If there is no child node, it will not be processed, and then insert the text node of the vnode as a child node under the corresponding real DOM node.

Part of the source code analysis in patch functions, the following stickers patchVnode source function analysis:

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 algorithm-update strategy of new and old vnode child nodes

When the old and new vnodes have child nodes, the diff algorithm defines four pointers to handle the child nodes. The four pointers are: oldStartVnode (old vnode) / newStartVnode (new vnode) / oldEndVnode (old vnode) / newEndVnode (new post vnode). After entering the loop, the child nodes of the new and old vnodes are compared in pairs. Here is a set of comparison rules, as shown in the figure below:
image.png
If the above four rules are not met, take the child node of oldVnode from the old front index oldStartIdx to the old back index oldEndIdx to map the key to the corresponding position number oldKeyToIdx , and use the key of the new vnode to find whether there is a corresponding in oldKeyToIdx The index value, if not, indicates that oldVnode does not have a corresponding old node, and it is a new node for inserting; if it has, it indicates that oldVnode has a corresponding old node, not a new node, and move operation is performed. Post the source code analysis below:

// 旧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)
      }
    }
  }

废废
187 声望3 粉丝