• 简单diff算法


    1.Diff算法介绍

    vue中用于比较新旧vnode的子节点都是一组节点时,为了以最小的性能开销完成更新,需要比较两个子节点,用与比较的算法就叫作diff算法。

    2.简单diff算法几种不同情况

    • 节点顺序、个数相同,节点文本不同,直接更新节点文本
    • 节点顺序相同,但个数不同(又分为新节点多或者旧节点多)
    • 节点个数相同,但顺序不同(又分为标签类型相同和标签类型不同)

    2.1 节点顺序、个数相同,节点文本不同,复用节点更改文本

    看一个例子,通过这组虚拟节点进行更新的步骤(节点顺序相同,且个数相同)

    1. const oldVNode = {
    2. type: 'div',
    3. children: [
    4. { type: 'p', children: '1' },
    5. { type: 'p', children: '2' },
    6. { type: 'p', children: '3' }
    7. ]
    8. }
    9. const newVNode = {
    10. type: 'div',
    11. children: [
    12. { type: 'p', children: '4' },
    13. { type: 'p', children: '5' },
    14. { type: 'p', children: '6' }
    15. ]
    16. }
    1. function patchChildren(n1, n2) {
    2. if (typeof n2.chilren === 'string') {
    3. // 省略部分代码
    4. } else if (Array.isArray(n2.children)) {
    5. // 更新逻辑
    6. const oldChildren = n1.children
    7. const newChildren = n2.children
    8. for (let i = 0; i < newChildren.length; i++) {
    9. // 更新子节点
    10. patch(oldChildren[i], newChildren[i])
    11. }
    12. } else {
    13. // 省略部分代码
    14. }
    15. }

    2.2 如果节点的个数对不上怎么解决?(节点顺序相同,但个数不同)

    原理:多余的新节点挂载,多余的旧节点卸载,其他的直接复用

    步骤:

    • 获取新旧节点长度,Math.min()获取相同长度commonLength的节点;
    • 直接遍历更新commonLength的相关节点;
    • 如果有多余新节点则挂载,如果有多余旧节点则卸载;

     代码实现:

    1. function patchChildren(n1, n2) {
    2. if (typeof n2.chilren === 'string') {
    3. // 省略部分代码
    4. } else if (Array.isArray(n2.children)) {
    5. // 更新逻辑
    6. const oldChildren = n1.children
    7. const newChildren = n2.children
    8. // 旧节点的长度
    9. const oldlen = oldChildren.length
    10. // 新节点的长度
    11. const newlen = newChildren.length
    12. const commonLength = Math.min(oldlen, newlen)
    13. for (let i = 0; i < commonLength; i++) {
    14. // 更新节点
    15. patch(oldChildren[i], newChildren[i])
    16. }
    17. // newlen > oldlen, 需要挂载
    18. if (newlen > oldlen) {
    19. for (let i = commonLength; i < newlen; i++) {
    20. patch(null, newChildren[i])
    21. }
    22. } else if (oldlen > newlen) {
    23. // oldlen > newlen, 需要卸载
    24. for (let i = commonLength; i < oldlen; i++) {
    25. unmount(null, oldChildren[i])
    26. }
    27. }
    28. } else {
    29. // 省略部分代码
    30. }
    31. }

    2.3 如果虚拟节点的顺序发生变化怎么解决?(节点个数相同,但顺序不同)

    分为两种情况:

    1. 新旧虚拟节点标签类型相同;
    2. 新旧虚拟节点标签类型不同;

    比较方法:通过key值进行比较

    1. // type不同的虚拟节点
    2. // oldVNode
    3. [
    4. { type: 'p' },
    5. { type: 'div' },
    6. { type: 'span' },
    7. ]
    8. // newChildren
    9. [
    10. { type: 'span' },
    11. { type: 'p' },
    12. { type: 'div' },
    13. ]
    14. // type相同数据不同的虚拟节点
    15. // oldVNode
    16. [
    17. { type: 'p', children: '1' },
    18. { type: 'p', children: '2' },
    19. { type: 'p', children: '3' }
    20. ]
    21. // newChildren
    22. [
    23. { type: 'p', children: '3' },
    24. { type: 'p', children: '2' },
    25. { type: 'p', children: '1' }
    26. ]

    图示: 

    思路分析

    第一步:取新的一组子节点中的第一个节点p-3,它的key为3。尝试在旧的一组子节点中找到具有相同key值的可复用节点,如果能找到,记录旧子节点中当前节点的索引为2

    第二步:取新的一组子节点中的第二个节点p-1,它的key为1。尝试在旧的一组子节点中找到具有相同key值的可复用节点,如果能找到,记录旧子节点中当前节点的索引为0

    此时索引值的递增顺序被打破。节点p-1在旧children中的索引是0,它小于节点p-3在旧children中的索引2。说明p-1在旧children中排在节点p-3前面,但在新的children中,他排在节点p-3后面。 所以,我们得到结论:节点p-1对应的真实DOM需要移动

    第三步:取新的一组子节点中的第二个节点p-2,它的key为2。尝试在旧的一组子节点中找到具有相同key值的可复用节点,如果能找到,记录旧子节点中当前节点的索引为1

    同理,节点p-2在旧children中排在节点p-3前面,但在新的children中,它排在节点p-3后面。因此,节点p-2对应的真实DOM也需要移动

    此时索引值的递增顺序被打破。节点p-1在旧children中的索引是0,它小于节点p-3在旧children中的索引2。说明p-1在旧children中排在节点p-3前面,但在新的children中,他排在节点p-3后面。 所以,我们得到结论:节点p-1对应的真实DOM需要移动

    关键点:

    关键点:找最大的索引值,索引值小于最大的索引值,表示需要移动真实DOM

    把新节点一个一个得去旧节点中找

    lastIndex:用来存储寻找过程中当前节点在旧的Children中最大的索引值。比如第一次 i 的循环找到的最大索引为2,但是第二轮找到的节点为0,那么0<2就证明是需要移动。(为什么?因为新节点按照 i 的循环,第二次循环的节点是递增的,但是结果0<2说明旧节点的不是递增,证明新节点和旧节点的顺序不一样,就需要更新)

    代码——查找虚拟节点不同

    1. function patchChildren(n1, n2) {
    2. if (typeof n2.chilren === 'string') {
    3. // 省略部分代码
    4. } else if (Array.isArray(n2.children)) {
    5. // 更新逻辑
    6. const oldChildren = n1.children
    7. const newChildren = n2.children
    8. // 用来存储寻找过程中当前节点在旧的Children中最大的索引值
    9. let lastIndex = 0
    10. for (let i = 0; i < newChildren.length; i++) {
    11. const newVNode = newChildren[i]
    12. for (let j = 0; j < oldChildren.length; j++) {
    13. const oldVNode = oldChildren[i]
    14. if (newVNode.key === oldVNode.key) {
    15. patch(oldVNode, newVNode)
    16. if (j < lastIndex) {
    17. // 如果当前找到的节点在旧Children中的索引小于lastIndex
    18. // 说明我们的当前节点对应的真实dom是需要移动的
    19. } else {
    20. lastIndex = j
    21. }
    22. }
    23. break;
    24. }
    25. }
    26. } else {
    27. // 省略部分代码
    28. }
    29. }

    如何移动元素?

    思路分析——移动旧节点对应真实DOM

    第一步:首先将旧子节点上的真实dom赋值给对应新节点的dom(可复用的旧节点,为什么要将旧子节点的真实DOM赋值给新节点的DOM?因为最终操作DOM是通过新节点的真实DOM去操作)

    第二步:找到需要移动的虚拟节点,将它上一个虚拟节点对应的真实DOM的下一个兄弟节点作为锚点

    第三步:将当前虚拟节点对应的真实dom移动到锚点位置

    (这里是通过找到需要移动的虚拟节点newVnode的上一个节点的位置,如果上一个节点不存在,表示已经是第一个节点不需要移动,如果存在则将该节点的真实DOM移动到上一个节点的下一个兄弟节点的位置)

    vnode里面的el存储的是真实的DOM节点,移动的时候就是将旧的虚拟节点的el属性赋值给新的虚拟节点的el;最终是通过新子节点的虚拟节点的顺序来操作真实DOM。

    问题?如何移动真实DOM这里移动的真实DOM指的是旧的虚拟DOM对应真实DOM?然后移动完成以后将旧的真实DOM再赋值给对应新节点的DOM?

    回答:diff算法中对比出新旧节点位置不一致后,要么移动新节点的真实DOM,要么移动旧节点的真实DOM去保证新旧节点的DOM的顺序保持一致的,而vue底层是移动旧节点的真实DOM的顺序
    真实DOM移动完成后不会将旧节点的真实DOM赋值给新节点。diff算法本身存在就是为了对比新旧节点是否相同,如果节点相同只需要复用节点DOM更改节点里面的内容即可,从而保证效率。

    最终代码:

    1. function patchChildren(n1, n2) {
    2. if (typeof n2.chilren === 'string') {
    3. // 省略部分代码
    4. } else if (Array.isArray(n2.children)) {
    5. // 更新逻辑
    6. const oldChildren = n1.children
    7. const newChildren = n2.children
    8. // 用来存储寻找过程中最大的索引值
    9. let lastIndex = 0
    10. for (let i = 0; i < newChildren.length; i++) {
    11. const newVNode = newChildren[i]
    12. for (let j = 0; j < oldChildren.length; j++) {
    13. const oldVNode = oldChildren[i]
    14. if (newVNode.key === oldVNode.key) {
    15. patch(oldVNode, newVNode)
    16. if (j < lastIndex) {
    17. // 如果当前找到的节点在旧Children中的索引小于lastIndex
    18. // 说明我们的当前节点对应的真实dom是需要移动的
    19. // 先获取当前newVNode的上一个节点
    20. const prevVNode = newChildren[i - 1]
    21. if (prevVNode) {
    22. // 如果prevVNode不存在,说明prevVNode就是第一个,不需要移动
    23. const anchor = prevVNode.el.nextSibling
    24. insert(newVNode.el, anchor) //将DOM插入到锚点位置
    25. //vnode里面的el存储的是真实的DOM节点,移动的时候就是将旧的虚拟节点的el属性赋值给新的虚拟节点的el;最终是通过新子节点的虚拟节点的顺序来操作真实DOM。
    26. }
    27. } else {
    28. lastIndex = j
    29. }
    30. }
    31. break;
    32. }
    33. }
    34. } else {
    35. // 省略部分代码
    36. }
    37. }

    3.我的简单示例

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    6. <title>Documenttitle>
    7. head>
    8. <body>
    9. <script>
    10. // 1.节点类型和顺序相同,只是文本内容不同,可以复用节点直接更新文本即可
    11. /*
    12. const oldVNode = {
    13. type: 'div',
    14. children: [
    15. { type: 'p', children: '1' },
    16. { type: 'p', children: '2' },
    17. { type: 'p', children: '3' }
    18. ]
    19. }
    20. const newVNode = {
    21. type: 'div',
    22. children: [
    23. { type: 'p', children: '4' },
    24. { type: 'p', children: '5' },
    25. { type: 'p', children: '6' }
    26. ]
    27. }
    28. */
    29. function patchChildren1(n1, n2) {
    30. if (typeof n1.children === "string") {
    31. //省略部分代码
    32. } else if (Array.isArray(n1.children)) {
    33. let newChildren = n1.children;
    34. let oldChildren = n2.children;
    35. for (let i = 0; i < newChildren.length; i++) {
    36. patch(newChildren[i], oldChildren[i]);
    37. }
    38. } else {
    39. //省略部分代码
    40. }
    41. }
    42. // 2.如果节点的个数对不上怎么解决?(节点顺序相同,但个数不同)
    43. /*
    44. 步骤:
    45. 获取新旧节点长度,Math.min()获取相同长度commonLength的节点;
    46. 直接遍历更新commonLength的相关节点;
    47. 如果有多余新节点则挂载,如果有多余旧节点则卸载;
    48. */
    49. function patchChildren2(n1, n2) {
    50. if (typeof n1.children === "string") {
    51. //省略部分代码
    52. } else if (Array.isArray(n1.children)) {
    53. let newLen = n1.children.length;
    54. let oldLen = n2.children.length;
    55. let commonLen = Math.min(newLen, oldLen);
    56. for (let i = 0; i < commonLen.length; i++) {
    57. patch(newChildren[i], oldChildren[i]);
    58. }
    59. // 如果有多余新节点进行挂载(注意挂载的节点是用新旧节点做对比,并以commonLen为基准)
    60. if (newLen > oldLen) {
    61. for (let i = commonLen; i < newLen; i++) {
    62. insert(null, newChildren[i]);
    63. }
    64. }
    65. // 如果有多余旧节点,进行卸载
    66. if (oldLen > newLen) {
    67. for (let i = commonLen; i < oldLen; i++) {
    68. unmount(null, oldChildren[i])
    69. }
    70. }
    71. } else {
    72. //省略部分代码
    73. }
    74. }
    75. // 3.如果虚拟节点的顺序发生变化怎么解决?(节点个数相同,但顺序不同)
    76. /*
    77. 有两种情况:新旧虚拟节点标签类型相同;新旧虚拟节点标签类型不同;
    78. 原理:循环套用,然后根据上一次查询的节点所在位置的索引,如果本次找到的节点索引位置小于上一次的,说明顺序不对需要移动DOM节点
    79. // type不同的虚拟节点
    80. // oldVNode
    81. [
    82. { type: 'p' },
    83. { type: 'div' },
    84. { type: 'span' },
    85. ]
    86. // newChildren
    87. [
    88. { type: 'span' },
    89. { type: 'p' },
    90. { type: 'div' },
    91. ]
    92. // type相同数据不同的虚拟节点
    93. // oldVNode
    94. [
    95. { type: 'p', children: '1' },
    96. { type: 'p', children: '2' },
    97. { type: 'p', children: '3' }
    98. ]
    99. // newChildren
    100. [
    101. { type: 'p', children: '3' },
    102. { type: 'p', children: '2' },
    103. { type: 'p', children: '1' }
    104. ]
    105. */
    106. function patchChildren3(n1, n2) {
    107. let newChildrenNode = n1.children;
    108. let oldChildrenNode = n2.children;
    109. // 用于记录索引位置
    110. let indexId = 0;
    111. for(let i=0; ilength;i++){
    112. // 当前新节点(记录节点用于后面移动)
    113. let newVnode = newChildrenNode[i];
    114. for(let j=0;jlength;j++){
    115. // 当前旧节点
    116. let oldVnode = oldChildrenNode[i];
    117. // 当前索引为j,如果当前索引小于上一次记录的indexId则表示DOM顺序有问题
    118. if(j
    119. // 如何移动?将当前找到的旧节点移动到当前新节点的上一个兄弟节点的位置
    120. // 找到当前新节点的上一个兄弟节点
    121. let preVnode = newChildren[i-1].el.nextSibling();
    122. if (preVnode) { //如果prevVnode不存在说明上一个兄弟节点不存在,即已经在最上面位置
    123. // 然后将当前旧节点移动到新节点的上一个兄弟节点
    124. insert(newVnode.el, preVnode); //将当前虚拟节点对应的真实dom移动到锚点位置
    125. }
    126. }else{
    127. indexId = j;
    128. }
    129. }
    130. }
    131. }
    132. script>
    133. body>
    134. html>

  • 相关阅读:
    Ae 效果:CC Overbrights
    「连载」边缘计算(二十)02-23:边缘部分源码(源码分析篇)
    回字文判断
    Python爬虫基础(四):使用更方便的requests库
    【Matplotlib绘制图像大全】(十七):散点图
    Spring boot 启动流程及外部化配置
    快速排序从零实现+应用(超详细)
    Java创建对象的最佳方式:单例模式(Singleton)
    acm拿国奖的第一关:数组和字符串
    Linux脚本shell中将Windos格式字符转换为unix
  • 原文地址:https://blog.csdn.net/qq_34569497/article/details/134070417