1. function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  2. var oldStartIdx = 0;//旧节点开始index
  3. var newStartIdx = 0;//新节点开始index
  4. var oldEndIdx = oldCh.length - 1;//旧节点结束index
  5. var oldStartVnode = oldCh[0];//旧节点开始节点VNode
  6. var oldEndVnode = oldCh[oldEndIdx];//旧节点结束节点
  7. var newEndIdx = newCh.length - 1;//新节点结束index
  8. var newStartVnode = newCh[0];//新节点开始节点VNode
  9. var newEndVnode = newCh[newEndIdx];//新节点结束虚拟节点VNode
  10. //核心dom-diff 算法
  11. //新旧节点两个指针,做比较
  12. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  13. //1.旧开始节点 === undefined
  14. if (isUndef(oldStartVnode)) {
  15. oldStartVnode = oldCh[++oldStartIdx];
  16. //2.旧结束节点 === undefined
  17. } else if (isUndef(oldEndVnode)) {
  18. oldEndVnode = oldCh[--oldEndIdx];
  19. //3.旧开始节点 === 新开始节点
  20. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  21. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
  22. oldStartVnode = oldCh[++oldStartIdx];
  23. newStartVnode = newCh[++newStartIdx];
  24. //4.旧结束节点 === 新结束节点
  25. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  26. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
  27. oldEndVnode = oldCh[--oldEndIdx];
  28. newEndVnode = newCh[--newEndIdx];
  29. //5.旧开始节点 === 新结束节点
  30. } else if (sameVnode(oldStartVnode, newEndVnode)) {
  31. patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
  32. //操作真实dom:将老开始节点放置在老结束节点的后面,占了老节点的结束节点位置
  33. canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
  34. oldStartVnode = oldCh[++oldStartIdx];
  35. newEndVnode = newCh[--newEndIdx];
  36. //6.旧结束节点 === 新开始节点
  37. } else if (sameVnode(oldEndVnode, newStartVnode)) {
  38. patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
  39. //操作真实dom:将结束节点放置在开始节点前面,因为这里指针有移动,作用就是原本的位置
  40. canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
  41. oldEndVnode = oldCh[--oldEndIdx];
  42. newStartVnode = newCh[++newStartIdx];
  43. //7.都不是
  44. } else {
  45. if (isUndef(oldKeyToIdx)) {
  46. //返回老节点的key-index的映射表
  47. oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
  48. }
  49. // 通过map来查找Vnode、如果没找到也要通过循环查找
  50. idxInOld = isDef(newStartVnode.key)
  51. ? oldKeyToIdx[newStartVnode.key]
  52. : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
  53. //index不存在,就是新增的元素
  54. if (isUndef(idxInOld)) {
  55. //实操dom:新增VNode,并且添加到dom中
  56. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
  57. } else {
  58. //不是新增元素,则移动
  59. //需要移动的VNode
  60. vnodeToMove = oldCh[idxInOld];
  61. //比较需要移动的VNode和现在新开始节点是否相同
  62. if (sameVnode(vnodeToMove, newStartVnode)) {
  63. //打补丁,以及遍历子节点
  64. patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
  65. // 将该下标的值设为undefined,标记着这个Vnode已经被处理过了,避免后面重复处理
  66. oldCh[idxInOld] = undefined;
  67. //实操dom
  68. canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
  69. } else {
  70. // key相同,但是元素类型不相同,同样是视为需要重新创建节点
  71. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
  72. }
  73. }
  74. newStartVnode = newCh[++newStartIdx];
  75. }
  76. }
  77. /*
  78. 1.如果开始下标大于结束下标,说明遍历老节点遍历结束
  79. 2.老节点遍历完毕,新节点的下标+1的值,添加进去
  80. 3.如果新节点遍历完了,就删除老节点中开始到结束下标的值
  81. */
  82. if (oldStartIdx > oldEndIdx) {
  83. refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
  84. addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
  85. } else if (newStartIdx > newEndIdx) {
  86. //老的没有遍历完,新的遍历完了
  87. //删除老的的节点,从start开始,end结束,包括end
  88. //这里原先移动了节点,用undefined占位,直接删除不影响任何节点
  89. removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  90. }
  91. }

新旧节点数组都配置首尾两个索引
新节点的两个索引:newStartIdx , newEndIdx
旧节点的两个索引:oldStartIdx,oldEndIdx
以两边向中间包围的形式 来进行遍历
头部的子节点比较完毕,startIdx 就加1
尾部的子节点比较完毕,endIdex 就减1
只要其中一个数组遍历完(startIdxvue2.0 diff - 图1
首先考虑,不移动DOM
其次考虑,移动DOM
最后考虑,新建 / 删除 DOM

旧头 == 新头

patchVnode,newStartIdx ++ , oldStartIdx ++
递归更新Vnode,oldStartIdx旧头索引加一,newStartIdx 新头索引加一
如果不考虑多层DOM 结构,新旧两个头一样的话,就直接进行下一轮循环vue2.0 diff - 图2

旧尾 == 新尾

和头头相同的处理是一样的,尾尾相同,直接进行下个循环
vue2.0 diff - 图3

旧头 == 新尾

这种情况为了复用,就要移动dom了
real Dom 真实dom的el现在储存在oldVode列表
dom方法中不存在insertAfter,所以使用insertBefore
即放在 oldEndVnode 后一个节点的前面
然后更新两个索引
oldStartIdx++,newEndIdx--

node.insertBefore(newnode,existingnode) existingnode 可选。在其之前插入新节点的子节点。如果未规定,则 insertBefore 方法会在结尾插入 newnode。

  1. // 源码,应该是为了压缩代码
  2. nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  3. // 简化
  4. parentElm.insertBefore(
  5. oldStartVnode.elm,
  6. oldEndVnode.elm.nextSibling
  7. );

vue2.0 diff - 图4

旧尾 == 新头

和上面的情况类似,也要移动dom
vue2.0 diff - 图5

单个遍历查找

前面四种逻辑都不满足的情况
用新子节点的子项,在旧子节点数组中遍历,找出一样的节点
1、生成旧子节点数组以 vnode.key 为key 的 map 表
2、拿到新子节点数组中 一个子项,判断它的key是否在上面的map 中
3、不存在,则新建DOM
4、存在,继续判断是否 sameVnode

生成map 表

经过 createKeyToOldIdx 生成一个 map 表 oldKeyToIdx
{ vnodeKey: 数组Index }

  1. const oldCh = [{
  2. tag:"div", key:1
  3. },{
  4. tag:"strong", key:2
  5. },{
  6. tag:"span", key:4
  7. }]
  8. const oldKeyToIdx = {
  9. 1:0,
  10. 2:1,
  11. 4:2
  12. }

去匹配map 表,判断是否有相同节点
oldKeyToIdx[newStartVnode.key]

key不存在旧子节点数组中

直接创建DOM,并插入oldStartVnode 前面
createElm(newStartVnode, parentElm, oldStartVnode.elm);

如果有多个新建节点,但是每次插入都是在oldStartVnode.elm前面,顺序如何保证
因为oldStartIdx是递增的,所以这个插入的顺序是正确的
vue2.0 diff - 图6

key存在旧子节点数组中

找到这个旧子节点,然后判断和新子节点是否 sameVnode
如果相同,直接移动到 oldStartVnode 前面
如果不同,直接创建插入 oldStartVnode 前面

处理可能剩下的节点

在updateChildren 中,比较完新旧两个数组之后,可能某个数组会剩下部分节点没有被处理过,所以这里需要统一处理

新子节点遍历完了

newStartIdx > newEndIdx
新子节点遍历完毕,旧子节点可能还有剩
所以我们要对可能剩下的旧节点进行 批量删除。
就是遍历剩下的节点,逐个删除DOM
vue2.0 diff - 图7

旧子节点遍历完了

oldStartIdx > oldEndIdx
旧子节点遍历完毕,新子节点可能有剩
所以要对剩余的新子节点处理
剩余的新子节点不存在旧子节点中,所以全部新建

新建有一个问题,就是插在哪里
var newEnd = newCh[newEndIdx + 1]
refElm = newEnd ? newEnd.elm :null;
refElm 获取的是 newEndIdx 后一位的节点
当前没有处理的节点是 newEndIdx
也就是说 newEndIdx+1 的节点如果存在的话,肯定被处理过了
vue2.0 diff - 图8

总结

vue2.0 diff - 图9

头头比较和尾尾比较

vue2.0 diff - 图10

旧头新尾比较

vue2.0 diff - 图11

旧尾新头比较

vue2.0 diff - 图12

无法复用的节点

vue2.0 diff - 图13
newStartIdx> newEndIdx ,结束循环

批量删除可能剩下的老节点