最近温习到虚构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)}//查看反复keyfunction 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