https://mp.weixin.qq.com/s/uU2wcs3AX-RRvbu_loQXAQ

Diff 干嘛的

比较前后 DOM 节点变化

设计思路

需求情况

  1. 节点属性变化
  2. 节点增删
  3. 节点移动

思路

  1. 判断情况
  2. 根据情况进行对应的逻辑处理

    前提

    上面思路基于优先级全都相同,实际开发不是这样

    按这个方案,其实有个隐含的前提—— 「不同操作的优先级是相同的」。但在日常开发中,「节点移动」发生较少,所以Diff算法会优先判断其他情况。 基于这个理念,主流框架(React、Vue)的Diff算法都会经历多轮遍历,先处理「常见情况」,后处理「不常见情况」。 所以,这就要求「处理不常见情况的算法」需要能给各种边界case兜底。 换句话说,完全可以仅使用「处理不常见情况的算法」完成Diff操作。主流框架之所以没这么做是为了性能考虑。 本文会砍掉「处理常见情况的算法」,保留「处理不常见情况的算法」。 这样,只需要40行代码就能实现Diff的核心逻辑。

code

虚拟节点模拟

  1. /*
  2. Placement对于新生成的node,代表对应DOM需要插入到页面中。对于已有的node,代表对应DOM需要在页面中移动
  3. Deletion代表node对应DOM需要从页面中删除
  4. */
  5. type Flag = "Placement" | "Delection";
  6. interface Node {
  7. key: string; //node的唯一标识,用于将节点在变化前、变化后关联上
  8. flag?: Flag;
  9. index?: number;
  10. }

如果一个node同时存在于beforeafterkey相同),我们称这个node可复用。

完整代码

  1. /*
  2. Placement对于新生成的node,代表对应DOM需要插入到页面中。对于已有的node,代表对应DOM需要在页面中移动
  3. Deletion代表node对应DOM需要从页面中删除
  4. */
  5. type Flag = "Placement" | "Deletion";
  6. interface _Node {
  7. key: string; //node的唯一标识,用于将节点在变化前、变化后关联上
  8. flag?: Flag;
  9. index?: number;
  10. }
  11. type NList = _Node[]; //NodeList
  12. /**
  13. * 接受前后的 NodeList 为他们打上标记
  14. * @param before 更新前的 NodeList
  15. * @param after
  16. */
  17. function diff(before: NList, after: NList): NList {
  18. /* 遍历前的准备工作 */
  19. //保存结果
  20. const result: NList = [];
  21. //将before 保存到 map 中,以O(1)复杂度就能通过key找到before中对应node
  22. const beforeMap = new Map<string, _Node>();
  23. before.forEach((node, i) => {
  24. node.index = i;
  25. beforeMap.set(node.key, node);
  26. });
  27. /* 遍历 after */
  28. // 遍历到的最后一个可复用node在before中的index
  29. let lastPlacedIndex = 0;
  30. for (let i = 0; i < after.length; i++) {
  31. const afterNode = after[i];
  32. afterNode.index = i;
  33. //代表该可复用的node在before中的对应node
  34. const beforeNode = beforeMap.get(afterNode.key);
  35. if (beforeNode) {
  36. // 存在可复用node
  37. // 从map中剔除该 可复用node
  38. beforeMap.delete(beforeNode.key);
  39. const oldIndex = beforeNode.index as number;
  40. /* 遍历核心逻辑 */
  41. if (oldIndex < lastPlacedIndex) {
  42. // 移动
  43. afterNode.flag = "Placement";
  44. result.push(afterNode);
  45. continue;
  46. } else {
  47. // 不移动
  48. lastPlacedIndex = oldIndex;
  49. }
  50. } else {
  51. // 不存在可复用node,这是一个新节点
  52. afterNode.flag = "Placement";
  53. result.push(afterNode);
  54. }
  55. }
  56. /* 遍历完收尾 */
  57. // beforeMap 如果还有 node,就说明这些不能复用,标记删除就好了
  58. beforeMap.forEach(node => {
  59. node.flag = "Deletion";
  60. result.push(node);
  61. });
  62. return result;
  63. }
  64. //效果
  65. // 更新前
  66. const before = [{ key: "a" }];
  67. // 更新后
  68. const after = [{ key: "d" }];
  69. /* [
  70. // diff(before, after) 输出
  71. ({ key: "d", flag: "Placement" }, { key: "a", flag: "Deletion" })
  72. ];
  73. */
  74. // 更新前
  75. // const before: NList = [{ key: "a" }, { key: "b" }, { key: "c" }];
  76. // 更新后
  77. // const after = [{ key: "c" }, { key: "b" }, { key: "a" }];
  78. const df = diff(before, after);
  79. df;