首页 > 编程语言 >实现最简单的 React DOM Diff 算法

实现最简单的 React DOM Diff 算法

时间:2022-08-22 21:44:26浏览次数:68  
标签:index beforeNode const DOM afterNode React key VNodeList Diff

实现最简单的 React DOM Diff 算法

本文写于 2022 年 08 月 22 日

定义 VNode 与 VNodeList 类型

首先我们定义一个简单的 VNode 类型。

type Flag = "Placement" | "Deletion";

interface VNode {
  key: string;
  flag?: Flag;
  index?: number;
}

type VNodeList = VNode[];
  • 这里 flag 代表了操作类型,如果是 "Placement" 意味着移动或者创建新的节点,如果是 "Deletion" 则是移除该节点
  • index 则是该 Node 在 VNodeList 中的 index(将数组存入 Map 之后会用到该属性)

构造测试数据

这里我们再构造两个 VNodeList 用于模拟用于 diff 的前后两颗虚拟 DOM 树。

const before: VNodeList = [{ key: "a" }, { key: "d" }];

const after: VNodeList = [{ key: "d" }, { key: "c" }];

我们期待他的输出应该是:

[
  { key: "c", index: 1, flag: "Placement" },
  { key: "a", index: 0, flag: "Deletion" },
];

完成 diff 函数

接下来该实现我们的超简单 diff 算法了,该函数签名如下。

function diff(before: VNodeList, after: VNodeList): VNodeList;

VNode 的三种操作

首先我们明确一个思路。

在 VNodeList 的 diff 过程当中,对于每个节点来说,只存在三种操作:创建、删除、向右移动。(这里为什么不存在向左移动呢?因为如果节点右移,那么我们就可以将向左移动理解为原地不动。)

然后我们就可以开始遍历 after 数组了,不过在此之间,可以通过 Hash 结构将 before 数组储存起来,这样我们遍历的时候可以直接通过 key 去寻找相同的节点。

function diff(before: VNodeList, after: VNodeList): VNodeList {
  const retList: VNodeList = [];

  const beforeMap: Map<string, VNode> = new Map();
  before.forEach((item, index) => {
    item.index = index;
    beforeMap.set(item.key, node);
  });

  for (let i = 0; i < after.length; i++) {
    const afterNode = after[i];
    afterNode.index = i;

    const beforeNode = beforeMap.get(afterNode.key);

    // TODO
  }

  return retList;
}

判断创建与删除

当我们从缓存中取到了 beforeNode 之后,会先进行一次判断:

  1. 如果没有 beforeNode,说明是全新节点,需要创建
  2. 如果有 beforeNode,说明是需要移动的节点
// ...
// const beforeNode = beforeMap.get(afterNode.key);

if (!beforeNode) {
  afterNode.flag = "Placement";
  retList.push(afterNode);
  continue;
}

beforeMap.delete(beforeNode.key);

// ...
// 循环结束

beforeMap.forEach((item) => {
  item.flag = "Deletion";
  retList.push(item);
});

创建节点非常简单,只需要修改 flag,而后将其放入需要 return 的结果数组即可进入下一个循环。

而需要移动的节点,则可以直接从 map 中删除,这样在循环结束后,map 中还剩下的 node 就是需要删除的了。

最后没有被以上条件筛选掉的话,就是需要进行我们的“右移”判断的 VNode 了。

右移判断

首先,我们使用一个变量来记录上一次右移的 beforeNode 的 index。

因为当前我们所遍历到的 afterNode,一定是当前遍历过的 VNode 中最右边的。

那么如果当前 VNode 所对应的 beforeNode,一定在上次右移的 beforeNode 的左边,可上次右移的 beforeNode 又在当前 afterNode 的左边。

相当于:

node1 -> node2

node2 -> node1

所以可以清晰的判断出 node1 是需要右移的。

beforeNode.index < lastPlacedIndex,则右移当前节点,否则当前节点位置不变。

let lastPlacedIndex = 0;

//  for (let i = 0; i < after.length; i++) {
// ...
// beforeMap.delete(beforeNode.key);

const oldIndex = beforeNode.index!;

if (oldIndex < lastPlacedIndex) {
  afterNode.flag = "Placement";
  retList.push(afterNode);
  continue;
}

lastPlacedIndex = oldIndex;

// ...
// 循环结束

完整代码入下:

function diff(before: VNodeList, after: VNodeList): VNodeList {
  const retList: VNodeList = [];

  const beforeMap: Map<string, VNode> = new Map();
  before.forEach((node, i) => {
    node.index = i;
    beforeMap.set(node.key, node);
  });

  let lastPlacedIndex = 0;

  for (let i = 0; i < after.length; i++) {
    const afterNode = after[i];
    afterNode.index = i;
    const beforeNode = beforeMap.get(afterNode.key);

    if (!beforeNode) {
      afterNode.flag = "Placement";
      retList.push(afterNode);
      continue;
    }

    beforeMap.delete(beforeNode.key);

    const oldIndex = beforeNode.index!;

    if (oldIndex < lastPlacedIndex) {
      afterNode.flag = "Placement";
      retList.push(afterNode);
      continue;
    }

    lastPlacedIndex = oldIndex;
  }

  beforeMap.forEach((item) => {
    item.flag = "Deletion";
    retList.push(item);
  });

  return retList;
}

如此这般操作,我们就成功实现了一个超级简单版的 React DOM Diff 算法。

(完)

标签:index,beforeNode,const,DOM,afterNode,React,key,VNodeList,Diff
From: https://www.cnblogs.com/xhyccc/p/16614356.html

相关文章

  • 手写实现react hooks
    实现一些简单的reacthooks........在钩子函数中不要使用if判断,避免钩子错乱建立数组映射,建立多组钩子初始化数组和索引,全局使用lethookIndex=0lethookState=[......
  • Uncaught DOMException: Failed to execute 'insertRule' on 'CSSStyleSheet': Cannot
    在动态的向某个元素添加动画的过程中,使用insertRule的方式插入,浏览器报错。具体报错如下:具体原因:这篇文章说的比较清楚了解决方案:insertCSSRule(element,cssStyle){......
  • LeetCode 811. Subdomain Visit Count
    原题链接在这里:https://leetcode.com/problems/subdomain-visit-count/题目:Awebsitedomain "discuss.leetcode.com" consistsofvarioussubdomains.Atthetople......
  • react面试题
    react事件机制在得到dom树之后,react会处理属性上是否有事件,react不会把事件绑定到真正的节点上,而是把所有的事件绑定在document(最外层节点)上,部分事件除外,如audio、video的......
  • React 源码-React 事件全解
    事件系统reactv17事件绑定事件绑定在函数setInitialDOMPropertiessetInitialDOMProperties将在complete阶段执行functionsetInitialDOMProperties(tag:st......
  • vscode中react标签自动补全
    在vscode中编写react时,发现标签没有自动补全,写起来不太灵活,查资料发现:默认在js文件中JSX语法无法自动补全,解决方法如下:文件=》首选项=》设置搜索"emmet.includeLanguages"......
  • react组件三大核心之一state
    -<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="wi......
  • crontab -e无法保存:/var/spool/cron/#tmp.localhost.localdomain.XXXXLjnf86: Operati
     问题:crontab-e无法保存:/var/spool/cron/#tmp.localhost.localdomain.XXXXLjnf86:Operationnotpermitted  原因:/var/spool/cron目录被chattr命令锁定,这一般......
  • puppeteer截取页面的DOM
    你还在用html2canvas软件进行截图吗?那你会遇到图片变糊了的问题,还有些样式方面的问题。可以采取服务端截图的方式来解决上述问题哦。即puppeteer截取页面的DOM大部分可能......
  • React报错之React Hook useEffect has a missing dependency
    正文从这开始~总览当useEffect钩子使用了一个我们没有包含在其依赖数组中的变量或函数时,会产生"ReactHookuseEffecthasamissingdependency"警告。为了解决该错误,禁......