https://juejin.cn/post/6971622260490797069#comment

vdom

image.png

Diff算法

新旧vdom的比较
oldVnode和vnode的比较

虚拟Dom

用一个对象将Dom抽象出来,是一个树形结构,一般包含tag、key、children,elm等属性

  1. {
  2. "tag": "div", // 标签
  3. "key": undefined, // key值
  4. "elm": div#app, // 真实DOM
  5. "text": undefined, // 文本信息
  6. "data": {attrs: {id:"app"}}, // 节点属性
  7. "children": [{ // 孩子属性
  8. "tag": "p",
  9. "key": undefined,
  10. "elm": p.text,
  11. "text": undefined,
  12. "data": {attrs: {class: "text"}},
  13. "children": [{
  14. "tag": undefined,
  15. "key": undefined,
  16. "elm": text,
  17. "text": "helloWorld",
  18. "data": undefined,
  19. "children": []
  20. }]
  21. }]
  22. }

Diff实现

image.png
比较是否相同节点

  1. // 比较是否相同节点
  2. function sameVnode(a, b) {
  3. return (
  4. a.key === b.key &&
  5. a.asyncFactory === b.asyncFactory && (
  6. (
  7. a.tag === b.tag &&
  8. a.isComment === b.isComment &&
  9. isDef(a.data) === isDef(b.data) &&
  10. sameInputType(a, b)
  11. ) || (
  12. isTrue(a.isAsyncPlaceholder) &&
  13. isUndef(b.asyncFactory.error)
  14. )
  15. )
  16. )
  17. }

核心方法

patch

是否相同节点,tag和key相同

  • 首先判断vnode是否存在,如果不存在的话,则代表这个旧节点要整个删除;
  • 如果vnode存在的话,再判断oldVnode是否存在,如果不存在的话,则代表只需要新增整个vnode节点就可以;
  • 如果vnode和oldVnode都存在的话,判断两者是不是相同节点,如果是的话,这调用patchVnode方法,对两个节点进行详细比较判断;
  • 如果两者不是相同节点的话,这种情况一般就是初始化页面,此时oldVnode其实是真实Dom,这是只需要将vnode转换为真实Dom然后替换掉oldVnode,具体就不多讲,这不是今天讨论的范围内。
  1. // 更新时调用的__patch__
  2. function patch(oldVnode, vnode, hydrating, removeOnly) {
  3. // 判断新节点是否存在
  4. if (isUndef(vnode)) {
  5. if (isDef(oldVnode)) invokeDestroyHook(oldVnode) // 新的节点不存在且旧节点存在:删除
  6. return
  7. }
  8. // 判断旧节点是否存在
  9. if (isUndef(oldVnode)) {
  10. // 旧节点不存在且新节点存在:新增
  11. createElm(vnode, insertedVnodeQueue)
  12. } else {
  13. if (sameVnode(oldVnode, vnode)) {
  14. // 比较新旧节点 diff算法
  15. patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  16. } else {
  17. // 初始化页面(此时的oldVnode是个真实DOM)
  18. oldVnode = emptyNodeAt(oldVnode)
  19. }
  20. // 创建新的节点
  21. createElm(
  22. vnode,
  23. insertedVnodeQueue,
  24. oldElm._leaveCb ? null : parentElm,
  25. nodeOps.nextSibling(oldElm)
  26. )
  27. }
  28. }
  29. return vnode.elm
  30. }

patchNode

更新节点属性

  • 首先判断两个虚拟Dom是不是全等,即没有任何变动;是的话直接结束函数,否则继续执行;
  • 其次更新节点的属性;
  • 接着判断vnode.text是否存在,即vnode是不是文本节点。是的话,只需要更新节点文本既可,否则的话,这继续比较;
  • 判断vnode和oldVnode是否有孩子节点:
  • 如果两者都有孩子节点的话,执行updateChildren()方法,进行比较更新孩子节点;
  • 如果vnode有孩子节点而oldVnode没有的话,则直接新增所有孩子节点,并将该节点文本属性设为空;
  • 如果oldVnode有孩子节点而vnode没有的话,则直接删除所有孩子节点;
  • 如果两者都没有孩子节点,就判断oldVnode.text是否有内容,有的话清空内容既可。

    1. // 比较两个虚拟DOM
    2. function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    3. // 如果两个虚拟DOM一样,无需比较直接返回
    4. if (oldVnode === vnode) {
    5. return
    6. }
    7. // 获取真实DOM
    8. const elm = vnode.elm = oldVnode.elm
    9. // 获取两个比较节点的孩子节点
    10. const oldCh = oldVnode.children
    11. const ch = vnode.children
    12. // 属性更新
    13. if (isDef(data) && isPatchable(vnode)) {
    14. for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    15. if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    16. }
    17. if (isUndef(vnode.text)) { // 没有文本 -> 该情况一般都是有孩子节点
    18. if (isDef(oldCh) && isDef(ch)) { // 新旧节点都有孩子节点 -> 比较子节点
    19. if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    20. } else if (isDef(ch)) { // 新节点有孩子节点,旧节点没有孩子节点 -> 新增
    21. if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // 如果旧节点有文本内容,将其设置为空
    22. addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    23. } else if (isDef(oldCh)) { // 旧节点有孩子节点,新节点没有孩子节点 -> 删除
    24. removeVnodes(oldCh, 0, oldCh.length - 1)
    25. } else if (isDef(oldVnode.text)) { // 旧节点有文本,新节点没有文本 -> 删除文本
    26. nodeOps.setTextContent(elm, '')
    27. }
    28. } else if (oldVnode.text !== vnode.text) { // 新旧节点文本不同 -> 更新文本
    29. nodeOps.setTextContent(elm, vnode.text)
    30. }
    31. }

updateChildren

Vue中,我们知道标签会有一个属性——key值,而在同一级的Dom中,如果key有值的话,它必须是唯一的;如果不设值就默认为undefined

  1. // 比较两组孩子节点
  2. function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  3. // 设置首尾4个指针和对应节点
  4. let oldStartIdx = 0
  5. let newStartIdx = 0
  6. let oldEndIdx = oldCh.length - 1
  7. let oldStartVnode = oldCh[0]
  8. let oldEndVnode = oldCh[oldEndIdx]
  9. let newEndIdx = newCh.length - 1
  10. let newStartVnode = newCh[0]
  11. let newEndVnode = newCh[newEndIdx]
  12. // diff查找是所需的变量
  13. let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  14. // 循环结束条件:新旧节点的头尾指针都重合
  15. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  16. if (isUndef(oldStartVnode)) {
  17. // 当oldStartVnode为undefined的时候,oldStartVnode右移
  18. oldStartVnode = oldCh[++oldStartIdx]
  19. } else if (isUndef(oldEndVnode)) {
  20. // 当oldEndVnode为undefined的时候,oldEndVnode左移
  21. oldEndVnode = oldCh[--oldEndIdx]
  22. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  23. // 当oldStartVnode与newStartVnode节点相同,对比节点
  24. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  25. // 对应两个指针更新
  26. oldStartVnode = oldCh[++oldStartIdx]
  27. newStartVnode = newCh[++newStartIdx]
  28. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  29. // 当oldEndVnode与newEndVnode节点相同,对比节点
  30. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
  31. // 对应两个指针更新
  32. oldEndVnode = oldCh[--oldEndIdx]
  33. newEndVnode = newCh[--newEndIdx]
  34. } else if (sameVnode(oldStartVnode, newEndVnode)) {
  35. // 当oldStartVnode与newEndVnode节点相同,对比节点
  36. patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
  37. // 将oldStartVnode节点移动到对应位置,即oldEndVnode节点的后面
  38. nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  39. // 对应两个指针更新
  40. oldStartVnode = oldCh[++oldStartIdx]
  41. newEndVnode = newCh[--newEndIdx]
  42. } else if (sameVnode(oldEndVnode, newStartVnode)) {
  43. // 当oldEndVnode与newStartVnode节点相同,对比节点
  44. patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  45. // 将oldEndVnode节点移动到对应位置,即oldStartVnode节点的前面
  46. nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  47. // 对应两个指针更新
  48. oldEndVnode = oldCh[--oldEndIdx]
  49. newStartVnode = newCh[++newStartIdx]
  50. } else { // 暴力解法 使用key匹配
  51. // 遍历剩余的旧孩子节点,将有key值的生成index表 <{key: i}>
  52. if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  53. // 如果newStartVnode存在key,就进行匹配index值;如果没有key值,遍历剩余的旧孩子节点,一一与newStartVnode匹配,相同节点的返回index
  54. idxInOld = isDef(newStartVnode.key)
  55. ? oldKeyToIdx[newStartVnode.key]
  56. : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
  57. if (isUndef(idxInOld)) {
  58. // 如果匹配不到index,则创建新节点
  59. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
  60. } else {
  61. // 获取对应的旧孩子节点
  62. vnodeToMove = oldCh[idxInOld]
  63. if (sameVnode(vnodeToMove, newStartVnode)) {
  64. patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  65. // 因为idxInOld是处于oldStartIdx和oldEndIdx之间,因此只能将其设置为undefined,而不是移动两个指针
  66. oldCh[idxInOld] = undefined
  67. nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
  68. } else {
  69. // 如果key相同但节点不同,就创建一个新的节点
  70. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
  71. }
  72. }
  73. // 移动新节点的左边指针
  74. newStartVnode = newCh[++newStartIdx]
  75. }
  76. }
  77. if (oldStartIdx > oldEndIdx) {
  78. // 当旧节点左指针已经超过右指针的时候,新增剩余的新的孩子节点
  79. refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  80. addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  81. } else if (newStartIdx > newEndIdx) {
  82. // 当新节点左指针已经超过右指针的时候,删除剩余的旧的孩子节点
  83. removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  84. }
  85. }

vue中key的作用

sameNode

通过tag和key比较是否是相同的node,如果是相同的node进入path步骤
如果不是新建一个节点

updateChildren

ch通过key去和oldCh比较,如果就得ch有,可以直接拿来用,如果旧的ch没有需要重新建一个ch再去插入