乐趣区

关于vue.js:详解虚拟DOM与Diff算法

最近温习到虚构 DOM 与 Diff,翻阅了泛滥材料,特此总结了这篇长文,加深本人对 vue 的了解。这篇文章比拟具体的剖析了 vue 的虚构 DOM,Diff 算法,其中一些要害的中央从别处搬运了一些图进行阐明(感激制图的大佬),也蕴含比拟具体的源码解读。

实在 DOM 的渲染

在讲虚构 DOM 之前,先说一下实在 DOM 的渲染。

浏览器实在 DOM 渲染的过程大略分为以下几个局部

  1. 构建 DOM 树。通过 HTML parser 解析解决 HTML 标记,将它们构建为 DOM 树(DOM tree),当解析器遇到非阻塞资源(图片,css), 会持续解析,然而如果遇到 script 标签(特地是没有 async 和 defer 属性),会阻塞渲染并进行 html 的解析,这就是为啥最好把 script 标签放在 body 上面的起因。
  2. 构建 CSSOM 树。与构建 DOM 相似, 浏览器也会将款式规定,构建成 CSSOM。浏览器会遍历 CSS 中的规定集,依据 css 选择器创立具备父子,兄弟等关系的节点树。
  3. 构建 Render 树 。这一步将 DOM 和 CSSOM 关联,确定每个 DOM 元素应该利用什么 CSS 规定。将所有相干款式匹配到 DOM 树中的每个可见节点,并依据 CSS 级联确定每个节点的计算款式,不可见节点(head, 属性包含 display:none 的节点) 不会生成到 Render 树中。
  4. 布局 / 回流 (Layout/Reflow)。浏览器第一次确定节点的地位以及大小叫布局,如果后续 节点地位以及大小发生变化 ,这一步触发布局调整,也就是 回流
  5. 绘制 / 重绘 (Paint/Repaint)。将元素的每个可视局部绘制到屏幕上,包含文本、色彩、边框、暗影和替换的元素(如按钮和图像)。如果 文本、色彩、边框、暗影 等这些元素发生变化时,会触发 重绘 (Repaint)。为了确保重绘的速度比初始绘制的速度更快,屏幕上的绘图通常被分解成数层。将内容晋升到 GPU 层(能够通过 tranform,filter,will-change,opacity 触发) 能够进步绘制以及重绘的性能。
  6. 合成(Compositing)。这一步将绘制过程中的分层合并,确保它们以正确的程序绘制到屏幕上显示正确的内容。

为啥须要虚构 DOM

下面这是一次 DOM 渲染的过程,如果 dom 更新,那么 dom 须要从新渲染一次,如果存在上面这种状况

<body>
    <div id="container">
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
                ....
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
    </div>
</body>
<script>
    let content = document.getElementsByClassName('content');
    for (let i = 0; i < 1000000; i++) {content[i].innerHTML = `This is a content${i}`;
        // 触发回流
        content[i].style.fontSize = `20px`;
    }
</script>

那么须要实在的操作 DOM100w 次, 触发了回流 100w 次。每次 DOM 的更新都会依照流程进行无差别的实在 dom 的更新。所以造成了很大的性能节约。如果循环外面是简单的操作,频繁触发回流与重绘,那么就很容易就影响性能,造成卡顿。另外这里要阐明一下的是,虚构 DOM 并不是意味着比 DOM 就更快,性能须要分场景,虚构 DOM 的性能跟模板大小是正相干。虚构 DOM 的比拟过程是不会辨别数据量大小的,在组件外部只有大量动静节点时,虚构 DOM 仍然是会对整个 vdom 进行遍历,相比间接渲染而言是多了一层操作的。

    <div class="list">
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">{{item}}</p>
    <p class="item">item</p>
    <p class="item">item</p>
  </div>

比方下面这个例子,虚构 DOM。尽管只有一个动静节点,然而虚构 DOM 仍然须要遍历 diff 整个 list 的 class,文本,标签等信息,最初仍然须要进行 DOM 渲染。如果只是 dom 操作,就只有操作一个具体的 DOM 而后进行渲染。虚构 DOM 最外围的价值在于,它能通过 js 形容实在 DOM,表达力更强,通过申明式的语言操作,为开发者提供了更加方便快捷开发体验,而且在没有手动优化,大部分情景下,保障了性能上限,性价比更高。

虚构 DOM

虚构 DOM 实质上是一个 js 对象,通过对象来示意实在的 DOM 构造。tag 用来形容标签,props 用来形容属性,children 用来示意嵌套的层级关系。

const vnode = {
    tag: 'div',
    props: {id: 'container',},
    children: [{
        tag: 'div',
        props: {class: 'content',},
          text: 'This is a container'
    }]
}

// 对应的实在 DOM 构造
<div id="container">
  <div class="content">
    This is a container
  </div>
</div>

虚构 DOM 的更新不会立刻操作 DOM,而是会通过 diff 算法,找出须要更新的节点,按需更新,并将更新的内容保留为一个 js 对象,更新实现后再挂载到实在 dom 上,实现实在的 dom 更新。通过虚构 DOM,解决了操作实在 DOM 的三个问题。

  1. 无差别频繁更新导致 DOM 频繁更新,造成性能问题
  2. 频繁回流与重绘
  3. 开发体验

另外因为虚构 DOM 保留的是 js 对象,人造的具备 跨平台 的能力, 而不仅仅局限于浏览器。

长处

总结起来,虚构 DOM 的劣势有以下几点

  1. 小批改无需频繁更新 DOM,框架的 diff 算法会主动比拟,剖析出须要更新的节点,按需更新
  2. 更新数据不会造成频繁的回流与重绘
  3. 表达力更强,数据更新更加不便
  4. 保留的是 js 对象,具备跨平台能力

有余

虚构 DOM 同样也有毛病,首次渲染大量 DOM 时,因为多了一层虚构 DOM 的计算,会比 innerHTML 插入慢。

虚构 DOM 实现原理

次要分三局部

  1. 通过 js 建设节点形容对象
  2. diff 算法比拟剖析新旧两个虚构 DOM 差别
  3. 将差别 patch 到实在 dom 上实现更新

Diff 算法

为了防止不必要的渲染,按需更新,虚构 DOM 会采纳 Diff 算法进行虚构 DOM 节点比拟, 比拟节点差别,从而确定须要更新的节点,再进行渲染。vue 采纳的是 深度优先,同层比拟 的策略。

新节点与旧节点的比拟次要是围绕三件事来达到渲染目标

  1. 创立新节点
  2. 删除废节点
  3. 更新已有节点

如何比拟新旧节点是否统一呢?

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) // 对 input 节点的解决
            ) || (isTrue(a.isAsyncPlaceholder) &&
                isUndef(b.asyncFactory.error)
            )
        )
    )
}

// 判断两个节点是否是同一种 input 输出类型
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
    //input type 雷同或者两个 type 都是 text
    return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

能够看到,两个节点是否雷同是须要比拟 标签 (tag) 属性 (在 vue 中是用 data 示意 vnode 中的属性 props), 正文节点 (isComment) 的, 另外碰到 input 的话,是会做非凡解决的。

创立新节点

当新节点有的,旧节点没有,这就意味着这是全新的内容节点。只有元素节点,文本节点,正文节点能力被创立插入到 DOM 中。

删除旧节点

当旧节点有,而新节点没有,那就意味着,新节点放弃了旧节点的一部分。删除节点会连带的删除旧节点的子节点。

更新节点

新的节点与旧的的节点都有,那么所有以新的为准,更新旧节点。如何判断是否须要更新节点呢?

  • 判断新节点与旧节点是否完全一致, 一样的话就不须要更新
  // 判断 vnode 与 oldVnode 是否齐全一样
  if (oldVnode === vnode) {return;}
  • 判断新节点与旧节点是否是动态节点,key 是否一样,是否是克隆节点 (如果不是克隆节点,那么意味着渲染函数被重置了,这个时候须要从新渲染) 或者是否设置了 once 属性, 满足条件的话替换 componentInstance
  // 是否是动态节点,key 是否一样,是否是克隆节点或者是否设置了 once 属性
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }
  • 判断新节点是否有文本(通过 text 属性判断),如果有文本那么须要比拟同层级旧节点,如果旧节点文本不同于新节点文本,那么采纳新的文本内容。如果新节点没有文本,那么前面须要对子节点的相干状况进行判断
// 判断新节点是否有文本
if (isUndef(vnode.text)) {
  // 如果没有文本,解决子节点的相干代码
  ....
} else if (oldVnode.text !== vnode.text) {
  // 新节点文本替换旧节点文本
  nodeOps.setTextContent(elm, vnode.text)
}
  • 判断新节点与旧节点的子节点相干情况。这里又能分为 4 种状况

    1. 新节点与旧节点 都有子节点
    2. 只有新节点有 子节点
    3. 只有旧节点有 子节点
    4. 新节点与旧节点 都没有子节点

都有子节点

对于都有子节点的状况,须要对新旧节点做比拟,如果他们不雷同,那么须要进行 diff 操作,在 vue 中这里就是 updateChildren 办法,前面会具体再讲,子节点的比拟次要是双端比拟。

// 判断新节点是否有文本
if (isUndef(vnode.text)) {
    // 新旧节点都有子节点状况下,如果新旧子节点不雷同,那么进行子节点的比拟,就是 updateChildren 办法
    if (isDef(oldCh) && isDef(ch)) {if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    }
} else if (oldVnode.text !== vnode.text) {
    // 新节点文本替换旧节点文本
    nodeOps.setTextContent(elm, vnode.text)
}

只有新节点有子节点

只有新节点有子节点,那么就代表着这是新增的内容,那么就是新增一个子节点到 DOM,新增之前还会做一个反复 key 的检测,并做出揭示,同时还要思考,旧节点如果只是一个文本节点,没有子节点的状况,这种状况下就须要清空旧节点的文本内容。

// 只有新节点有子节点
if (isDef(ch)) {
  // 查看反复 key
  if (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(ch)
  }
  // 革除旧节点文本
  if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
  // 增加新节点
  addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}

// 查看反复 key
function checkDuplicateKeys(children) {const seenKeys = {}
  for (let i = 0; i < children.length; i++) {const vnode = children[i]
      // 子节点每一个 Key
      const key = vnode.key
      if (isDef(key)) {if (seenKeys[key]) {
              warn(`Duplicate keys detected: '${key}'. This may cause an update error.`,
                  vnode.context
              )
          } else {seenKeys[key] = true
          }
      }
  }
}

只有旧节点有子节点

只有旧节点有,那就阐明,新节点摈弃了旧节点的子节点,所以须要删除旧节点的子节点

if (isDef(oldCh)) {
  // 删除旧节点
  removeVnodes(oldCh, 0, oldCh.length - 1)
}

都没有子节点

这个时候须要对旧节点文本进行判断,看旧节点是否有文本,如果有就清空

if (isDef(oldVnode.text)) {
  // 清空
  nodeOps.setTextContent(elm, '')
}

整体的逻辑代码如下

function patchVnode(
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
) {
    // 判断 vnode 与 oldVnode 是否齐全一样
    if (oldVnode === vnode) {return}

    if (isDef(vnode.elm) && isDef(ownerArray)) {
        // 克隆重用节点
        vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {if (isDef(vnode.asyncFactory.resolved)) {hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
        } else {vnode.isAsyncPlaceholder = true}
        return
    }
        // 是否是动态节点,key 是否一样,是否是克隆节点或者是否设置了 once 属性
    if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
        vnode.componentInstance = oldVnode.componentInstance
        return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children

    if (isDef(data) && isPatchable(vnode)) {
          // 调用 update 回调以及 update 钩子
        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)) {
          // 新旧节点都有子节点状况下,如果新旧子节点不雷同,那么进行子节点的比拟,就是 updateChildren 办法
        if (isDef(oldCh) && isDef(ch)) {if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        } else if (isDef(ch)) {
              // 只有新节点有子节点
            if (process.env.NODE_ENV !== 'production') {
                  // 反复 Key 检测
                checkDuplicateKeys(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)
    }

    if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
}

配上流程图会更清晰点

子节点的比拟更新 updateChildren

新旧节点都有子节点的状况下,这个时候是须要调用 updateChildren 办法来比拟更新子节点的。所以在数据上,新旧节点子节点,就保留为了两个数组。

const oldCh = [oldVnode1, oldVnode2,oldVnode3];
const newCh = [newVnode1, newVnode2,newVnode3];

子节点更新采纳的是 双端比拟 的策略,什么是双端比拟呢,就是新旧节点比拟是通过相互比拟首尾元素 (存在 4 种比拟),而后向两头聚拢比拟(newStartIdx, 与 oldStartIdx 递增,newEndIdx 与 oldEndIdx 递加) 的策略。

比拟过程

向两头聚拢

这里对下面呈现的 新前,新后,旧前,旧后 做一下阐明

  1. 新前,指的是 新节点未解决 的子节点数组中的 第一个 元素,对应到 vue 源码中的newStartVnode
  2. 新后,指的是 新节点未解决 的子节点数组中的 最初一个 元素, 对应到 vue 源码中的newEndVnode
  3. 旧前,指的是 旧节点未解决 的子节点数组中的 第一个 元素, 对应到 vue 源码中的oldStartVnode
  4. 旧后,指的是 旧节点未解决 的子节点数组中的 最初一个 元素, 对应到 vue 源码中的oldEndVnode

子节点比拟过程

接下来对下面的比拟过程以及比拟后做的操作做下阐明

  • 新前与旧前 的比拟,如果他们雷同,那么进行下面说到的 patchVnode 更新操作,而后新旧节点各向后一步,进行第二项的比拟 … 直到遇到不同才会换种比拟形式

if (sameVnode(oldStartVnode, newStartVnode)) {
  // 更新子节点
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  // 新旧各向后一步
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
}
  • 新后与旧后 的比拟,如果他们雷同,同样进行 pathchVnode 更新,而后新旧各向前一步,进行前一项的比拟 … 直到遇到不同,才会换比拟形式

if (sameVnode(oldEndVnode, newEndVnode)) {
    // 更新子节点
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
    // 新旧向前
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
}
  • 新后与旧前 的比拟,如果它们雷同,就进行更新操作,而后将旧前挪动到 所有未解决旧节点数组最初面,使旧前与新后地位保持一致,而后单方一起向两头聚拢,新向前,旧向后。如果不同会持续切换比拟形式

if (sameVnode(oldStartVnode, newEndVnode)) {patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
  // 将旧子节点数组第一个子节点挪动插入到最初
  canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  // 旧向后
  oldStartVnode = oldCh[++oldStartIdx]
  // 新向前
  newEndVnode = newCh[--newEndIdx]
  • 新前与旧后 的比拟,如果他们雷同,就进行更新,而后将旧后挪动到 所有未解决旧节点数组最后面,是旧后与新前地位保持一致,,而后新向后,旧向前,持续向两头聚拢。持续比拟残余的节点。如果不同,就应用传统的循环遍历查找。

if (sameVnode(oldEndVnode, newStartVnode)) {patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  // 将旧后挪动插入到最前
  canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  // 旧向前
  oldEndVnode = oldCh[--oldEndIdx]
  // 新向后
  newStartVnode = newCh[++newStartIdx]
}
  • 循环遍历查找,下面四种都没找到的状况下,会通过 key 去查找匹配。

进行到这一步对于没有设置 key 的节点,第一次会通过 createKeyToOldIdx 建设 key 与 index 的映射 {key:index}

// 对于没有设置 key 的节点,第一次会通过 createKeyToOldIdx 建设 key 与 index 的映射 {key:index}
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

而后拿新节点的 key 与旧节点进行比拟,找到 key 值匹配的节点的地位,这里须要留神的是,如果新节点也没 key,那么就会执行 findIdxInOld 办法,从头到尾遍历匹配旧节点

// 通过新节点的 key, 找到新节点在旧节点中所在的地位下标, 如果没有设置 key,会执行遍历操作寻找
idxInOld = isDef(newStartVnode.key)
  ? oldKeyToIdx[newStartVnode.key]
  : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

//findIdxInOld 办法
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
  }
}

如果通过下面的办法,仍旧没找到新节点与旧节点匹配的下标,那就阐明这个节点是新节点,那就执行新增的操作。

// 如果新节点无奈在旧节点中找到本人的地位下标, 阐明是新元素,执行新增操作
if (isUndef(idxInOld)) {createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}

如果找到了,那么阐明在旧节点中找到了 key 值一样,或者节点和 key 都一样的旧节点。如果节点一样,那么在 patchVnode 之后,须要将旧节点 挪动到所有未解决节点之前,对于 key 一样,元素不同的节点,将其认为是新节点,执行新增操作。执行实现后,新节点向后一步。

// 如果新节点无奈在旧节点中找到本人的地位下标, 阐明是新元素,执行新增操作
if (isUndef(idxInOld)) {
  // 新增元素
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
  // 在旧节点中找到了 key 值一样的节点
  vnodeToMove = oldCh[idxInOld]
  if (sameVnode(vnodeToMove, newStartVnode)) {
    // 雷同子节点更新操作
    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    // 更新完将旧节点赋值 undefined
    oldCh[idxInOld] = undefined
    // 将旧节点挪动到所有未解决节点之前
    canMove && 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)
}

子节点比拟总结

下面就是子节点比拟更新的一个残缺过程,这是残缺的逻辑代码

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)) {
            // 新后与旧前
            // 更新子节点
            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)) {
            // 新前与旧后
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)

            // 将旧后挪动插入到最前
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)

            // 新向后,旧向前
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {// 对于没有设置 key 的节点,第一次会通过 createKeyToOldIdx 建设 key 与 index 的映射 {key:index}
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            // 通过新节点的 key, 找到新节点在旧节点中所在的地位下标, 如果没有设置 key,会执行遍历操作寻找
            idxInOld = isDef(newStartVnode.key) ?
                oldKeyToIdx[newStartVnode.key] :
                findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

            // 如果新节点无奈在旧节点中找到本人的地位下标, 阐明是新元素,执行新增操作
            if (isUndef(idxInOld)) {
                // 新增元素
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
                // 在旧节点中找到了 key 值一样的节点
                vnodeToMove = oldCh[idxInOld]
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    // 雷同子节点更新操作
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                        // 更新完将旧节点赋值 undefined
                    oldCh[idxInOld] = undefined
                        // 将旧节点挪动到所有未解决节点之前
                    canMove && 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)
    }
}

参考资料

  1. VirtualDOM 与 diff
  2. 渲染页面:浏览器的工作原理
  3. Vue 中的 DOM-Diff
  4. 深刻分析:Vue 外围之虚构 DOM
退出移动版