当前位置:首页 > 通信资讯 > 正文

谈谈你对diff算法的理解(diffing算法)

谈谈你对diff算法的理解(diffing算法)

一、基础

Diff算法实现的是最小量更新虚拟DOM。这句话虽然简短,但是涉及到了两个核心要素:虚拟DOM、最小量更新。

1.虚拟DOM

虚拟DOM指的就是将真实的DOM树构造为js对象的形式,从而解决浏览器操作真实DOM的性能问题。

例如:如下DOM与虚拟DOM之间的映射关系

谈谈你对diff算法的理解(diffing算法)

2.最小量更新

Diff的用途就是在新老虚拟DOM之间找到最小更新的部分,从而将该部分对应的DOM进行更新。

二、整个流程

Diff算法真的很美,整个流程如下图所示:

谈谈你对diff算法的理解(diffing算法)

  1. 首先比较一下新旧节点是不是同一个节点(可通过比较sel(选择器)和key(唯一标识)值是不是相同),不是同一个节点则进行暴力删除(注:先以旧节点为基准插入新节点,然后再删除旧节点)。
  2. 若是同一个节点则需要进一步比较

完全相同,不做处理

新节点内容为文本,直接替换完事

新节点有子节点,这个时候就要仔细考虑一下了:若老节点没有子元素,则直接清空老节点,将新节点的子元素插入即可;若老节点有子元素则就需要按照上述的更新策略老搞定了(记住更新策略,又可以吹好几年了,666666)。

三、实战

光说不练假把式,下面直接输出diff算法的核心内容。

3.1 patch函数

Diff算法的入口函数,主要判断新旧节点是不是同一个节点,然后交由不同的逻辑进行处理。

  1. exportdefaultfunctionpatch(oldVnode,newVnode){
  2. //判断传入的第一个参数,是DOM节点还是虚拟节点
  3. if(oldVnode.sel===''||oldVnode.sel===undefined){
  4. //传入的第一个参数是DOM节点,此时要包装成虚拟节点
  5. oldVnode=vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode);
  6. }
  7. //判断oldVnode和newVnode是不是同一个节点
  8. if(oldVnode.key===newVnode.key&&oldVnode.sel===newVnode.sel){
  9. //是同一个节点,则进行精细化比较
  10. patchVnode(oldVnode,newVnode);
  11. }
  12. else{
  13. //不是同一个节点,暴力插入新的,删除旧的
  14. letnewVnodeElm=createElement(newVnode);
  15. //将新节点插入到老节点之前
  16. if(oldVnode.elm.parentNode&&newVnodeElm){
  17. oldVnode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm);
  18. }
  19. //删除老节点
  20. oldVnode.elm.parentNode.removeChild(oldVnode.elm);
  21. }
  22. }

3.2 patchVnode函数

该函数主要负责精细化比较,通过按照上述流程图中的逻辑依次判断属于哪一个分支,从而采取不同的处理逻辑。(思路清晰,算法太牛了)

  1. exportdefaultfunctionpatchVnode(oldVnode,newVnode){
  2. //判断新旧vnode是否是同一个对象
  3. if(oldVnode===newVnode){
  4. return;
  5. }
  6. //判断vnode有没有text属性
  7. if(newVnode.text!==undefined&&(newVnode.children===undefined||newVnode.children.length===0)){
  8. console.log('新vnode有text属性');
  9. if(newVnode.text!==oldVnode.text){
  10. oldVnode.elm.innerText=newVnode.text;
  11. }
  12. }
  13. else{
  14. //新vnode没有text属性,有children
  15. console.log('新vnode没有text属性');
  16. //判断老的有没有children
  17. if(oldVnode.children!==undefined&&oldVnode.children.length>0){
  18. //老的有children,新的也有children
  19. updateChildren(oldVnode.elm,oldVnode.children,newVnode.children);
  20. }
  21. else{
  22. //老的没有children,新的有children
  23. //清空老的节点的内容
  24. oldVnode.elm.innerHTML='';
  25. //遍历新的vnode的子节点,创建DOM,上树
  26. for(leti=0;i<newVnode.children.length;i++){
  27. letdom=createElement(newVnode.children[i]);
  28. oldVnode.elm.appendChild(dom);
  29. }
  30. }
  31. }
  32. }

3.3 updateChildren函数

核心函数,主要负责旧虚拟节点和新虚拟节点均存在子元素的情况,按照比较策略依次进行比较,最终找出子元素中变化的部分,实现最小更新。对于该部分,涉及到一些指针,如下所示:

谈谈你对diff算法的理解(diffing算法)

  1. 旧前指的就是更新前虚拟DOM中的头部指针
  2. 旧后指的就是更新前虚拟DOM中的尾部指针
  3. 新前指的就是更新后虚拟DOM中的头部指针
  4. 新后指的就是更新后虚拟DOM中的尾部指针

按照上述的更新策略,上述旧虚拟DOM更新为新虚拟DOM的流程为:

  1. 命中“新后旧前”策略,然后将信后对应的DOM节点(也就是节点1)移动到旧后节点(节点3)后面,然后旧前节点指针下移,新后节点指针上移。
  2. 仍然命中“新后旧前”策略,做相同的操作,将节点2移动到旧后节点(节点3)后面,下移旧前节点,上移新后节点。
  3. 命中“新前旧前”策略,DOM节点不变,旧前和新前节点均下移。
  4. 跳出循环,移动结束。
  1. exportdefaultfunctionupdateChildren(parentElm,oldCh,newCh){
  2. //旧前
  3. letoldStartIdx=0;
  4. //新前
  5. letnewStartIdx=0;
  6. //旧后
  7. letoldEndIdx=oldCh.length-1;
  8. //新后
  9. letnewEndIdx=newCh.length-1;
  10. //旧前节点
  11. letoldStartVnode=oldCh[0];
  12. //旧后节点
  13. letoldEndVnode=oldCh[oldEndIdx];
  14. //新前节点
  15. letnewStartVnode=newCh[0];
  16. //新后节点
  17. letnewEndVnode=newCh[newEndIdx];
  18. letkeyMap=null;
  19. while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){
  20. //略过已经加undefined标记的内容
  21. if(oldStartVnode==null||oldCh[oldStartIdx]===undefined){
  22. oldStartVnode=oldCh[++oldStartIdx];
  23. }
  24. elseif(oldEndVnode==null||oldCh[oldEndIdx]===undefined){
  25. oldEndVnode=oldCh[--oldEndIdx];
  26. }
  27. elseif(newStartVnode==null||newCh[newStartIdx]===undefined){
  28. newStartVnode=newCh[++newStartIdx];
  29. }
  30. elseif(newEndVnode==null||newCh[newEndIdx]===undefined){
  31. newEndVnode=newCh[--newEndIdx];
  32. }
  33. elseif(checkSameVnode(oldStartVnode,newStartVnode)){
  34. //新前与旧前
  35. console.log('新前与旧前命中');
  36. patchVnode(oldStartVnode,newStartVnode);
  37. oldStartVnode=oldCh[++oldStartIdx];
  38. newStartVnode=newCh[++newStartIdx];
  39. }
  40. elseif(checkSameVnode(oldEndVnode,newEndVnode)){
  41. //新后和旧后
  42. console.log('新后和旧后命中');
  43. patchVnode(oldEndVnode,newEndVnode);
  44. oldEndVnode=oldCh[--oldEndIdx];
  45. newEndVnode=newCh[--newEndVnode];
  46. }
  47. elseif(checkSameVnode(oldStartVnode,newEndVnode)){
  48. console.log('新后和旧前命中');
  49. patchVnode(oldStartVnode,newEndVnode);
  50. //当新后与旧前命中的时候,此时要移动节点,移动新后指向的这个节点到老节点旧后的后面
  51. parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling);
  52. oldStartVnode=oldCh[++oldStartIdx];
  53. newEndVnode=newCh[--newEndIdx];
  54. }
  55. elseif(checkSameVnode(oldEndVnode,newStartVnode)){
  56. //新前和旧后
  57. console.log('新前和旧后命中');
  58. patchVnode(oldEndVnode,newStartVnode);
  59. //当新前和旧后命中的时候,此时要移动节点,移动新前指向的这个节点到老节点旧前的前面
  60. parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm);
  61. oldEndVnode=oldCh[--oldEndIdx];
  62. newStartVnode=newCh[++newStartIdx];
  63. }
  64. else{
  65. //四种都没有命中
  66. //制作keyMap一个映射对象,这样就不用每次都遍历老对象了
  67. if(!keyMap){
  68. keyMap={};
  69. for(leti=oldStartIdx;i<=oldEndIdx;i++){
  70. constkey=oldCh[i].key;
  71. if(key!==undefined){
  72. keyMap[key]=i;
  73. }
  74. }
  75. }
  76. //寻找当前这项(newStartIdx)在keyMap中的映射的位置序号
  77. constidxInOld=keyMap[newStartVnode.key];
  78. if(idxInOld===undefined){
  79. //如果idxInOld是undefined表示踏实全新的项,此时会将该项创建为DOM节点并插入到旧前之前
  80. parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm);
  81. }
  82. else{
  83. //如果不是undefined,则不是全新的项,则需要移动
  84. constelmToMove=oldCh[idxInOld];
  85. patchVnode(elmToMove,newStartVnode);
  86. //把这项设置为undefined,表示已经处理完这项了
  87. oldCh[idxInOld]=undefined;
  88. //移动
  89. parentElm.insertBefore(elmToMove.elm,oldStartVnode.elm);
  90. }
  91. //指针下移,只移动新的头
  92. newStartVnode=newCh[++newStartIdx];
  93. }
  94. }
  95. //循环结束后,处理未处理的项
  96. if(newStartIdx<=newEndIdx){
  97. console.log('new还有剩余节点没有处理,要加项,把所有剩余的节点插入到oldStartIdx之前');
  98. //遍历新的newCh,添加到老的没有处理的之前
  99. for(leti=newStartIdx;i<=newEndIdx;i++){
  100. //insertBefore方法可以自动识别null,如果是null就会自动排到队尾去
  101. //newCh[i]现在还没有真正的DOM,所以要调用createElement函数变为DOM
  102. parentElm.insertBefore(createElement(newCh[i]),oldCh[oldStartIdx].elm);
  103. }
  104. }
  105. elseif(oldStartIdx<=oldEndIdx){
  106. console.log('old还有剩余节点没有处理,要删除项');
  107. //批量删除oldStart和oldEnd指针之间的项
  108. for(leti=oldStartIdx;i<=oldEndIdx;i++){
  109. if(oldCh[i]){
  110. parentElm.removeChild(oldCh[i].elm);
  111. }
  112. }
  113. }
  114. }

原文链接:https://mp.weixin.qq.com/s/RKbOTFAJJ7VEpAJdUI3iTA

如果您对该产品感兴趣,请填写办理(客服微信:xiaoxiongyidong)

为您推荐:

发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。