前置概念

Diff 比较目的是什么?

实际上创建一个 DOM 添加到页面上显示,其渲染过程是非常复杂的。通过 Diff 比较,找出最小差异 **VNode(虚拟DOM)**,把最小差异那部分 VNode 更新到 DOM 尽可能减少 DOM 更新渲染消耗,也可理解成是为了复用 DOM

Diff 比较做法是什么?

只有两个 新旧 **VNode(虚拟DOM)** 相等时,才会比较 VNode它的子节点,也叫 **同层级比较**

什么是同层级比较?

Vue Diff 比较 - 图1
上图 总共有 四次 比较

  • 第一次,绿色方框 相等,拿到 1 子节点,往下比较
  • 第二次,蓝色方框 旧节点 2 跟 新节点 2 相等,拿到 2 子节点,往下比较
  • 第三次,蓝色方框 旧节点 3 跟 新节点 3 相等,拿到 3 子节点,往下比较

Vue Diff 比较 - 图2
上图 只有 二 比较

  • 第一次,绿色方框 相等,拿到 1 子节点,往下比较
  • 第二次,蓝色方框 旧节点 跟 新节点 没有相等,则不会再往下比较

总结

  1. Diff 比较本质上是为了 复用 DOM 节点,所以要在找 旧节点 中找出 相同的节点
  2. 比较建立在 同层比较基础之上的,而不是为了找到相同节点,无限制递归查找

辅助API

下面是在 Diff 比较过程中 所需要用到的一些方法辅助,比如 创建DOM元素、比较节点是否相等 等等…

跨平台 nodeOps

Diff 比较之后如何更新到页面呢?

  1. **Web浏览器 **环境里,要操作 **真实DOM **只能是调用 浏览器提供的一些 API 例如:`**appendChild**`,所以比较的过程中 更新真实DOM,实际上还是调用 浏览器提供的 **原生API。**

除了 浏览器环境还会有别的环境 吗?

当然!比如说 **weex**由于使用了 **Virtual DOM **的原因,Vue.js具有了跨平台的能力,**Virtual DOM** 终归只是用来描述 真实DOM 一些 JavaScript 对象罢了。那么就需要有一个 适配层 ,来适配不同平台所需要调用 API 的差异,将不同平台的 API 封装在内,统一对外提供。如下面代码:

  1. const nodeOps = {
  2. setTextContent (text) {
  3. if (platform === 'weex') {
  4. node.parentNode.setAttr('value', text);
  5. } else if (platform === 'web') {
  6. node.textContent = text;
  7. }
  8. },
  9. parentNode () {
  10. //......
  11. },
  12. removeChild () {
  13. //......
  14. },
  15. nextSibling () {
  16. //......
  17. },
  18. insertBefore () {
  19. //......
  20. },
  21. createElement () {
  22. // .......
  23. }
  24. }

insert 插入节点

  1. function insert (parent, elm, ref) {
  2. if (isDef(parent)) { // 是否有 父节点,需要根据父节点插入节点
  3. if (isDef(ref)) { // 是否有传 参考兄弟节点
  4. if (nodeOps.parentNode(ref) === parent) { // 父节点是否相同,相同才是兄弟节点
  5. nodeOps.insertBefore(parent, elm, ref) // 将 elm 节点,插在兄弟节点前面
  6. }
  7. } else { // 没有 直接插入父节点末尾
  8. nodeOps.appendChild(parent, elm)
  9. }
  10. }
  11. }
  • Diff 比较的过程会使用该方法 将 DOM 节点插入到页面

creteElm 创建节点

  1. function createElm (vnode, parentElm, refElm){
  2. const children = vnode.children
  3. const tag = vnode.tag
  4. if (isDef(tag)) { // 是否是 普通节点
  5. vnode.elm = nodeOps.createElement(tag, vnode) // 创建DOM元素
  6. createChildren(vnode, children) // 创建子节点,并将子节点插入 vnode.elm
  7. insert(parentElm, vnode.elm, refElm) // 插入 vnode.elm 到页面
  8. } else if (isTrue(vnode.isComment)) { // 是否是 注释节点
  9. vnode.elm = nodeOps.createComment(vnode.text)
  10. insert(parentElm, vnode.elm, refElm) // 插入 注释节点
  11. } else { // 文本节点
  12. vnode.elm = nodeOps.createTextNode(vnode.text)
  13. insert(parentElm, vnode.elm, refElm) // 插入 文本节点
  14. }
  15. }
  • 文本、注释 VNode 都是没有 tag 属性的,因此用它来判断是不是 普通标签。
  • 文本节点:

Vue Diff 比较 - 图3

createChildren 创建子节点

  1. function createChildren (vnode, children) {
  2. if (Array.isArray(children)) { // 如果是数组,则遍历逐个创建
  3. for (let i = 0; i < children.length; ++i) {
  4. createElm(children[i], vnode.elm)
  5. }
  6. // // 如果是 string,number,symbol,boolean 其中一个一种类型都以 文本节点创建
  7. } else if (isPrimitive(vnode.text)) {
  8. nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  9. }
  10. }

removeNode 删除节点

  1. function removeNode (el) {
  2. const parent = nodeOps.parentNode(el) // 找到 节点的 父节点
  3. if (isDef(parent)) {
  4. nodeOps.removeChild(parent, el)
  5. }
  6. }

【sameVnode】比较 VNode 节点是否相等

  1. function sameVnode (a, b) {
  2. return
  3. a.key === b.key &&
  4. (
  5. a.tag === b.tag &&
  6. a.isComment === b.isComment &&
  7. isDef(a.data) === isDef(b.data) &&
  8. sameInputType(a, b)
  9. )
  10. }
  11. function sameInputType (a, b) {
  12. if (a.tag !== 'input') return true
  13. let i
  14. const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  15. const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  16. return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
  17. }
  • 主要判断四个点,key、tag、双方是否都存在 data、双方都是注释节点
  • key a.undefined === b.test为不相等,a.test === b.test为相等,a.undefined === b.undefined 为相等
  • tag a.div === b.span不相等,a.div === b.div 相等
  • data a.undefined === b.data不相等(这里的 data 不是数据的 data)
  • 还有一种情况 input 节点,如果是 input 就比较 type 是否相等

为什么这么判断两个 VNode 是否相等呢?

  1. 判断目的是 是要重新创建还是复用原 DOM
  2. data 判断是双方都有 data 对象,那么就是相等的节点,一个有一个没有那肯定不是同一个节点,因为在模版编译时就已经确定是否有 data 了,即便属性是动态的
  3. key 标识 和 tag 也是在 模版编译时 就知道是不是相同的节点

createKeyToOldIdx 生成 children(子节点)key 对应 index(索引)map

  1. function createKeyToOldIdx (children, beginIdx, endIdx) {
  2. let i, key
  3. /**
  4. * map 表
  5. * 结构:[{tag: 'div', key: 'id123'}, {tag: 'p', key: 'id456'}] -> {"id123": 0, "id456": 1}
  6. */
  7. const map = {}
  8. for (i = beginIdx; i <= endIdx; ++i) {
  9. key = children[i].key
  10. if (isDef(key)) map[key] = i // 只有节点有 key 都会存入 map 表
  11. }
  12. return map
  13. }

函数作用是什么?

前面提到 Diff 比较其实也是为了复用 DOM,假设有 大量 children(子节点)逐个比较下来 时间复杂度是 O(n2) 。这个时候就需要有个 id 来标识出两个节点是否相等,而 Key 就是这个标识。

那这样 比较 Key 相等 逐个比较下来不也是 O(n2) 复杂度吗?此时 该函数作用就体现出来 了,比较时 给 旧节点 生成(只生成一次) **key: index**map 表,再拿 新节点的 Key map 查找否存在 相同的 Key,如果存在 就能直接取到 相同Key 的 老节点 Index,再进行具体的比较,这样查找则变成 时间复杂 O(1)。

如果没有 Key ,还是会从老节点中逐个比较查找,从上面也能回答出 key 的作用

总结:key 和 createKeyToOldIdx 作用是为了在 新老节点 比较过程中 降低查找相同节点的复杂度


比较过程

createPatchFunction

  1. import * as nodeOps from 'web/runtime/node-ops'
  2. import platformModules from 'web/runtime/modules/index'
  3. const modules = platformModules.concat(baseModules)
  4. const createPatchFunction = function createPatchFunction (backend) {
  5. // ...
  6. return path(oldVnode, vnode) {
  7. // 没有旧节点
  8. if (isUndef(oldVnode)) {
  9. // oldVnode 未定义的时候,就是root节点,创建一个新的节点
  10. createElm(vnode, insertedVnodeQueue)
  11. } else {
  12. // oldVnode 是真实DOM 才为 true
  13. // nodeType 属性只有是真实 DOM 才有
  14. const isRealElement = isDef(oldVnode.nodeType)
  15. // 判断是真实 DOM 且 新旧VNode 相同
  16. if( !isRealElement && sameVnode(oldVnode, vnode)) {
  17. patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  18. } else {
  19. // 新旧节点不一样
  20. // 创建一个空的将 elm 保存在内
  21. oldVnode = emptyNodeAt(oldVnode)
  22. const oldElm = oldVnode.elm
  23. const parentElm = nodeOps.parentNode(oldElm)
  24. // 创建新节点
  25. createElm(vnode, parentElm, nodeOps.nextSibling(oldElm))
  26. // 销毁老节点
  27. if (isDef(parentElm)) {
  28. removeVnodes([oldVnode], 0, 0)
  29. }
  30. }
  31. }
  32. }
  33. }
  34. const path = createPatchFunction({
  35. nodeOps,
  36. modules,
  37. LONG_LIST_THRESHOLD: 10
  38. })
  39. Vue.prototype.__patch__ = patch

1 没有旧节点

没有旧节点时,说明页面是初始化的时候,此时,不需要比较,直接使用 createElm 全部新建

2 旧节点 和 新节点 一样

通过 sameVnode 对比是否一样,当为 ture 时 直接调用 patchVnode 去比较 子节点

3 旧节点 和 新节点 不一样

不一样,直接使用 createElm 创建一个新节点,接着再删除 旧节点

patchVnode

  1. function patchVnode (oldVnode, vnode){
  2. if (oldVnode === vnode) return;
  3. const elm = vnode.elm = oldVnode.elm
  4. // 新旧节点 都是注释节点
  5. if (isTrue(vnode.isStatic) && // 新节点 是注释节点
  6. isTrue(oldVnode.isStatic) && // 旧节点 是注释节点
  7. vnode.key === oldVnode.key && // 新旧节点 key 相等
  8. (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) // 新节点 是克隆 或者 有标记 v-once 属性
  9. ) {
  10. vnode.componentInstance = oldVnode.componentInstance // 直接使用 旧节点 组件实例见赋值到 新节点组件实例
  11. return
  12. }
  13. // 数据对象 更新
  14. let i
  15. const data = vnode.data
  16. if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  17. i(oldVnode, vnode)
  18. }
  19. const oldCh = oldVnode.children
  20. const ch = vnode.children
  21. // Vnode 是否 文本节点
  22. if (isUndef(vnode.text)) {
  23. // 都有 子节点
  24. if (isDef(oldCh) && isDef(ch)) {
  25. if (oldCh !== ch) updateChildren(elm, oldCh, ch)
  26. // 只有 新节点 有子节点
  27. } else if (isDef(ch)) {
  28. for (let startIdx = 0; startIdx <= ch.length-1; ++startIdx) {
  29. createElm(vnodes[startIdx], parentElm, refElm)
  30. }
  31. // 只有 旧VNode 有子节点
  32. } else if (isDef(oldCh)) {
  33. for (let startIdx = 0; startIdx <= oldCh.length - 1; ++startIdx) {
  34. removeNode(oldCh[startIdx].elm)
  35. }
  36. // 旧节点 有文本节点
  37. } else if (isDef(oldVnode.text)) {
  38. nodeOps.setTextContent(elm, '')
  39. }
  40. // 新旧 VNode 节点,文本不一致,直接替换文本
  41. } else if (oldVnode.text !== vnode.text) {
  42. nodeOps.setTextContent(elm, vnode.text)
  43. }
  44. }

Vnode 是文本节点。通过 text 属性 就可以判定是 文本节点,所以就有两种处理方式。

  • Vnode.text 存在,且 和 旧的 Vnode.text 不一样时,直接更新节点内容

nodeOps.setTextContent(elm, vnode.text)

  • Vnode.text 不存在,但 旧的 oldVnode.text 存在 时,直接将内容赋值为空

nodeOps.setTextContent(elm, '')

Vnode 存在子节点。因为不知道新旧子节点是否一样,所有这里有三种情况。

  • 只有新子节点。那么这里就没得比较,遍历逐个新建就好了,并把 父子节点也添加进去

  • 只有旧子节点。旧子节点有 而没有新的子节点,说明更新后节点都被删除了,此时 只需遍历通过 removeNode 逐个把旧子节点 删除

  • 新旧节点 都有子节点,而且不一样。当出现这种情况就不是单个比较了,而是通过 updateChildren 方法遍历逐个进行比较。

除了以上两种情况,还对 注释节点、 数据对象、也有考虑到进行相应处理。

  • 注释节点,如果 新旧节点都是注释节点 且 节点 key 相等,则直接使用 旧节点实例 componentInstance 结束比较。

  • 数据对象,包含 class、style、attrs 等等,在其内部也调用对应的方法进行比较更新 如:updateClass、updateAttrs 等等,当然上面源码没有呈现出来,源码路径:src\platforms\web\runtime\modules

updateChildren

  1. function updateChildren (parentElm, oldCh, newCh, removeOnly) {
  2. // 新旧 VNode 首指针
  3. let oldStartIdx = 0
  4. let newStartIdx = 0
  5. // 新旧 VNode 尾指针
  6. let oldEndIdx = oldCh.length - 1
  7. let newEndIdx = newCh.length - 1
  8. // 新旧 VNode 首节点
  9. let oldStartVnode = oldCh[0]
  10. let newStartVnode = newCh[0]
  11. // 新旧 VNode 尾节点
  12. let oldEndVnode = oldCh[oldEndIdx]
  13. let newEndVnode = newCh[newEndIdx]
  14. let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  15. const canMove = !removeOnly
  16. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  17. if (isUndef(oldStartVnode)) {
  18. // 旧VNode 首节点 不存在,旧节点 指针右移
  19. oldStartVnode = oldCh[++oldStartIdx]
  20. } else if (isUndef(oldEndVnode)) {
  21. // 旧VNode 尾节点 不存在,旧节点 指左针移
  22. oldEndVnode = oldCh[--oldEndIdx]
  23. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  24. // 新旧 VNode 首节点 一致,首首比较
  25. patchVnode(oldStartVnode, newStartVnode, newCh, newStartIdx)
  26. // 新旧 VNode 指针右移
  27. oldStartVnode = oldCh[++oldStartIdx]
  28. newStartVnode = newCh[++newStartIdx]
  29. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  30. // 新旧 VNode 尾节点 一致,尾尾比较
  31. patchVnode(oldEndVnode, newEndVnode, newCh, newEndIdx)
  32. // 新旧 VNode 指针左移
  33. oldEndVnode = oldCh[--oldEndIdx]
  34. newEndVnode = newCh[--newEndIdx]
  35. } else if (sameVnode(oldStartVnode, newEndVnode)) {
  36. // 旧 VNode 首节点 VS 新 VNode 尾节点 一致,首尾 交叉比较
  37. patchVnode(oldStartVnode, newEndVnode, newCh, newEndIdx)
  38. canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  39. // 旧 VNode 指针右移
  40. oldStartVnode = oldCh[++oldStartIdx]
  41. // 新 VNode 指针左移
  42. newEndVnode = newCh[--newEndIdx]
  43. } else if (sameVnode(oldEndVnode, newStartVnode)) {
  44. // 旧 VNode 尾节点 VS 新 VNode 首节点 一致,尾首 交叉比较
  45. patchVnode(oldEndVnode, newStartVnode, newCh, newStartIdx)
  46. canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  47. // 旧 VNode 指针左移
  48. oldEndVnode = oldCh[--oldEndIdx]
  49. // 新 VNode 指针右移
  50. newStartVnode = newCh[++newStartIdx]
  51. } else {
  52. /**
  53. * 生成一个 VNode.key :VNdoe 索引位置 对应的哈希表(只有第一次进来oldKeyToIdx = undefined 的时候会生成,也为后面检测重复的key值做铺垫)
  54. 比如 childre 是这样的 [{tag: 'div', key: 'id123'},{tag: 'div', key: 'id456'}] beginIdx = 0 endIdx = 2
  55. 结果生成{id123: 0, id456: 1}
  56. */
  57. if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  58. // 判断 新VNode 首节点 是否有 key,有 则拿此 key 去找 旧VNode key哈希表(oldKeyToIdx) 找到 旧VNode 位置
  59. // 没有 拿 新VNode 首节点,逐个跟 旧 VNode 比较,是否跟 新VNode 相同节点位置
  60. idxInOld = isDef(newStartVnode.key)
  61. ? oldKeyToIdx[newStartVnode.key]
  62. : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
  63. if (isUndef(idxInOld)) {
  64. // 没有从 旧VNode 找到相同节点,则创建一个新的节点
  65. createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
  66. } else {
  67. // 获取同key 的老节点
  68. vnodeToMove = oldCh[idxInOld]
  69. if (sameVnode(vnodeToMove, newStartVnode)) {
  70. // 如果 新VNode 与得到的有相同 key的 旧VNode 节点 是同一个 VNode 则进行 patchVnode
  71. patchVnode(vnodeToMove, newStartVnode, newCh, newStartIdx)
  72. oldCh[idxInOld] = undefined
  73. // 当有标识位 canMov e实可以直接插入 oldStartVnode 对应的真实 DOM 节点前面
  74. canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
  75. } else {
  76. // same key but different element. treat as new element
  77. // 当 新VNode 与找到的 同样key的 旧VNode 不是 sameVNode 的时候(比如说tag不一样或者是有不一样type的input标签)
  78. // 创建一个新的节点
  79. createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
  80. }
  81. }
  82. // 新首 VNode 指针右移动
  83. newStartVnode = newCh[++newStartIdx]
  84. }
  85. }
  86. if (oldStartIdx > oldEndIdx) {
  87. // 全部比较完成以后,发现 oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实Dom中
  88. refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  89. addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx)
  90. } else if (newStartIdx > newEndIdx) {
  91. // 如果全部比较完成以后发现 newStartIdx > newEndIdx,则说明新节点已经遍历完了,旧VNode 比较 新VNode 有多出来,将多余的老节点从真实 DOM 中移除
  92. removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  93. }
  94. }

函数处理了什么?

当出现 新子节点 和 旧子节点 都有的情况,在该函数 新旧子节点 进行,循环遍历逐个比较

如何 循环遍历?

  1. 使用 while

  2. 新旧节点数组 都配置 两个指向首尾索引 如下:

  • 新节点两个索引:newStartIdx、newEndIdx
  • 旧节点两个索引:oldStartIdx、oldEndIdx

两边向中间包围的形式 来进行遍历比较

首部子节点比较完毕,startIdx 就加 1
尾部子节点比较完结,endIdx 就减 1

只要其中一个数组遍历完 (startIdx <= endIdx)则结束遍历

图示:
Vue Diff 比较 - 图4

主要处理流程分为 两个

  1. 比较新旧节点 (主要流程)
  2. 比较完毕,处理剩下的节点

1. 比较新旧节点

注:在比较时 有两个数组,一个 新子节点数组 newCh、一个 旧子节点数组 oldCh

在比较过程中,不会对这两个数组做任何操作,不会添加,也不会删除
而所有比较过程都是 直接操作DOM,如:插入删除节点。

1.1 比较原则 / 比较逻辑

  1. 比较原则
  • 首先考虑,不移动 DOM,原地复用

  • 其次考虑,移动 DOM,减少新建

  • 最后考虑,新建 / 删除 DOM

总结:能不移动,尽量不移动。不行就移动,实在不行就只能新建

  1. 比较 5 种逻辑 ``` 1、旧头 == 新头

2、旧尾 == 新尾

3、旧头 == 新尾

4、旧尾 == 新头

5、单个查找

  1. <a name="SgGDh"></a>
  2. #### 1.2 旧头 == 新头
  3. ```javascript
  4. sameVnode(oldStartVnode, newStartVnode)
  • 当两个新旧的两个头一样的时候,并不用做什么处理
  • 也符合第一个原则,原地复用 DOM
  • 通过 patchVnode 继续处理 两个相同节点的 子节点,或者更新文本
  • 因为不考虑多层DOM 结构,所以 新旧两个头一样的话,这里就算结束了
    1. oldStartVnode = oldCh[++oldStartIdx]
    2. newStartVnode = newCh[++newStartIdx]
    图示:
    Vue Diff 比较 - 图5

1.3 旧尾 == 新尾

  1. sameVnode(oldEndVnode, newEndVnode)
  • 这步比较逻辑 处理相同结果 跟 头头比较是一样的
  • 尾尾相同,直接跳入下个循环
    1. oldEndVnode = oldCh[--oldEndIdx]
    2. newEndVnode = newCh[--newEndIdx]
    图示:
    Vue Diff 比较 - 图6

1.4 旧头 == 新尾

  1. sameVnode(oldStartVnode, newEndVnode)
  • 这步 节点相同 就不符合,原地复用 了,因为位置不一样,所以只能移动 DOM

    怎么移动?
  • 先是通过 patchVnode 更新节点信息

  • 再把 oldStartVnode(旧头节点) 放到 oldEndVnode.elm(旧尾节点) 的后面,因为浏览没有提供把 DOM 放到谁后面的方法,所以只能使用 insertBefore 属性 拿到 oldEndVnode.elm 下一个节点

    1. nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  • 最后再更新两个索引

    1. oldStartVnode = oldCh[++oldStartIdx]
    2. newEndVnode = newCh[--newEndIdx]

    图示:
    Vue Diff 比较 - 图7

1.5 旧尾 == 新头

  1. sameVnode(oldEndVnode, newStartVnode)
  • 同样不能符合 原地复用,只能 移动DOM

怎么移动?
  • 先是通过 patchVnode 更新节点信息

  • 再把 oldEndVnode(旧尾节点) 放到 oldStartVnode.elm(新头节点) 的前面

    1. nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  • 再更新两个索引

    1. oldEndVnode = oldCh[--oldEndIdx]
    2. newStartVnode = newCh[++newStartIdx]

图示:
Vue Diff 比较 - 图8

1.6 单个遍历查找

  • 当前面四种比较逻辑都不行的时候,这是最后一种处理方法,拿 新头节点,直接去 旧子节点数组中遍历,找一样的节点出来,这里为了 降低查找的复杂度 ,引入了一种方法,在查找之前 先生成,以 旧节点 key 对应 节点 索引 的一个 map 表,大概流程如下: ``` 1、生成旧子节点数组以 vnode.key 为key 的 map 表

2、拿到新子节点数组中 首节点,判断它的key是否在上面的 map 中

3、不存在,则新建DOM

4、存在,继续判断是否 sameVnode

  1. <a name="J0y1b"></a>
  2. #####
  3. <a name="Xw6fD"></a>
  4. ##### 1 生成 map 表
  5. - 这个 **map **表的作用,就主要是用来判断 **新节点.key** 跟 **旧子节.key 是否有相同,**以此 来判断 **节点是否相同**
  6. 如 旧节点数组:
  7. ```javascript
  8. [{
  9. tag: "div", key: "id123"
  10. },{
  11. tag: "strong", key: "id456"
  12. },{
  13. tag: "span", key: "id789"
  14. }]

生成 map 表

  1. oldKeyToIdx = {
  2. "id123": 0,
  3. "id456": 1,
  4. "id789": 2
  5. }

2. 判断 新节点 是否存在 旧节点数组中
  • 根据 新头节点.key 去匹配 map 表,判断是否有相同节点 ```javascript idxInOld = oldKeyToIdx[newStartVnode.key]

if (isUndef(idxInOld)) { // 不存在 } else { // 存在 }

  1. <a name="UP1pc"></a>
  2. ##### 3 不存在 旧节点数组中
  3. - 直接创建**DOM**,并插入**oldStartVnode **前面
  4. ```javascript
  5. createElm(newStartVnode, parentElm, oldStartVnode.elm)

图示:
Vue Diff 比较 - 图9

4 存在 旧节点数组中
  • 存在则根据 map 表,找到 旧节点,通过 sameVnode 比较(新头 比较 map 表旧节点)

  • 如果相同,进行 patchVnode 更新节点信息,再将节点移动到 oldStartVnode 前面(因为是拿 新头节点 去比较的)

  • 如果不同,直接创建插入 oldStartVnode 前面 ```javascript vnodeToMove = oldCh[idxInOld]

if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode) nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { createElm(newStartVnode, parentElm, oldStartVnode.elm) }

  1. - 再更新 新头节点 索引
  2. ```javascript
  3. newStartVnode = newCh[++newStartIdx]

2. 处理可能剩下的节点

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

2.1 新子节点遍历完了

  1. while (newStartIdx <= newEndIdx) {}
  • 新子节点 遍历完毕,旧子节点可能还有剩 旧节点,则进行 批量删除!
    1. for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {
    2. oldCh[oldStartIdx].parentNode.removeChild(el);
    3. }
    图示:
    Vue Diff 比较 - 图10

2.2 旧子节点遍历完了

  1. while (oldStartIdx <= oldEndIdx) {}
  • 旧子节点遍历完毕,新子节点可能有剩,剩余的新子节点 直接 全部新建!
    1. for (; newStartIdx <= newEndIdx; ++newStartIdx) {
    2. createElm(newCh[newStartIdx], parentElm, refElm);
    3. }

但是有一个问题,就是这些新节点插在哪里?refElm 如何获取?
  1. refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  • refElm 获取的是 newEndIdx 旧节点 的后一位节点
  • 如果 newEndIdx+1 的节点如果存在的话,肯定被比较处理过了,那么就逐个添加在 refElm 的前面

  • 如果 newEndIdx 没有移动过,一直是最后一位,那么就 不存在 newCh[newEndIdx + 1]

  • 那么 不存在 refElm 就是空,剩余的新节点 就全部添加进 末尾。

图示
Vue Diff 比较 - 图11

为什么这么比较?

所有的比较,都是为了找到 新子节点 和 旧子节点 一样的子节点

比较三个原则

1、能不移动,尽量不移动,头头 、尾尾比较

2、没得办法,只好移动,头尾、尾头 交叉比较

3、实在不行,新建或删除,相同 key 比较,不同 则 新增、删除

流程

点击查看【processon】