Diff算法的设计思想 VS 40行代码实现React核心Diff算法
来源:魔术师卡颂(ID:gh_52d0bec584f9)
任何依赖于DOM虚拟需求的框架❝》对比《节点变化前后》的Diff算法。
网上解释❀❝算法逻辑的文章很多,无论作者的语言多么简洁,图片多么雄辩
今天我们改用一劳永逸的学习方法——实现Diff算法React.
不难,只有40行代码,你不觉得吗?往下看。由Juithm st设计,想象一下Diff算法要考虑多少种情况?大概有三种类型,即:
- 节点属性的变化,如:
// 更新前
<ul>
<li key="0" className="before">0</li>
<li key="1">1</li>
</ul>
// 更新后
<ul>
<li key="0" className="after">0</li>
<li key="1">1</li>
</ul>
- 如:节点的添加和删除,节点移动,如:
// 更新前
<ul>
<li key="0">0</li>
<li key="1">1</li>
</ul>
// 更新后
<ul>
<li key="1">1</li>
<li key="0">0</li>
</ul>
如何设计Diff算法?鉴于只有以上三种情况是常见的设计思路:
- 首先判断当前节点属于哪种情况
- 如果是增删则执行增删逻辑
- 如果是属性改变,执行属性改变逻辑
- 如果是移动,执行移动逻辑
根据这个方案,其实有一个隐含的假设——“不同操作的优先级是相同的” 。但在日常开发中,“结运动”发生的频率较低,因此Diff算法会优先考虑其他情况。
基于这个理念,主流框架(React、Vue)的 也就是说,只需要使用“tumm操作”,完全可以完成Diff算法会经过几轮的pass,先处理后处理,后处理 “异常情况”。?Diffmon操作”
本文将砍掉“处理常见情况的算法”并保留❝❝❀❝动作.
这样只需要40行代码就可以实现Diff的核心逻辑。
演示介绍
首先我们定义虚拟DOM节点的数据结构:
type Flag = 'Placement' | 'Deletion';
interface Node {
key: string;
flag?: Flag;
index?: number;
}
keydeis 。节点变化前后变化是相关的。
节点 我们希望实现 例如: 执行后结果为:页面a变为d。 又如: 由于 由于 执行后的结果是:页面上的abc变为cba。 核心逻辑包括三个步骤: 我们拯救每一个 这样,以 遍历 例如下面的例子中,b是可复用的: 对于 如果这个 注意: 更新后, 这意味着 遍历结束后,如果Map❝之前的 例如下面的情况,遍历完 这意味着 所以,最后,添加标签删除逻辑: 完整代码请参见在线演示地址 [1] 按照flag表示Diff❀❝遍历后,对对应的对应真实DOM进行操作Diff❀❝遍历Location对于新生成的节点 ,必须在页面上插入相应的DOM。对于现有的节点,DOM对应的代表必须在页面上移动。 删除代表节点DOM必须从删除页面下载※❝的索引位置 中同级节点注:本Demo之前接收function diff(before: NodeList, after: NodeList): NodeList {
const result: NodeList = [];
// ...遍历前的准备工作
for (let i = 0; i < after.length; i++) {
// ...核心遍历逻辑
}
// ...遍历后的收尾工作
return result;
}
仅实现了标志为“”,“根据标志执行DOM操作” 是未实现。
diff方法来在更新和在更新ist之后接收,标记它们旗帜: type NodeList = Node[];
function diff(before: NodeList, after: NodeList): NodeList {
// ...代码
}
// 更新前
const before = [
{key: 'a'}
]
// 更新后
const after = [
{key: 'd'}
]
// diff(before, after) 输出
[
{key: "d", flag: "Placement"},
{key: "a", flag: "Deletion"}
]
{key: "d", flag: "Location"} 表示 d 对应DOMs,必须在第 页面提交。 {key: "a", flag: "Delete"}表示DOM对应的a必须删除。 // 更新前
const before = [
{key: 'a'},
{key: 'b'},
{key: 'c'},
]
// 更新后
const after = [
{key: 'c'},
{key: 'b'},
{key: 'a'}
]
// diff(before, after) 输出
[
{key: "b", flag: "Placement"},
{key: "a", flag: "Placement"}
]
b已经存在,{key: "b", flag: “Location”♝ 对应 b❝ DOM 必须向后移动(对应方法 是parentNode.appendChild)。执行此操作后,abc 变为 acb。 a 之前已经存在,{key: "a", flag: "Location"} 代表 和 DOM 需要向后移动。执行此操作后,acb 更改为 cba。不同算法的实现
node 位于 之前 中,位于 node.key,因为 ❝ node价值 在地图中。 O(1)的复杂度,我们就可以在❝❝❝ function diff(before: NodeList, after: NodeList): NodeList {
const result: NodeList = [];
// ...遍历前的准备工作
for (let i = 0; i < after.length; i++) {
// ...核心遍历逻辑
}
// ...遍历后的收尾工作
return result;
}
中找到对应的
节点: // 保存结果
const result: NodeList = [];
// 将before保存在map中
const beforeMap = new Map<string, Node>();
before.forEach((node, i) => {
node.index = i;
beforeMap.set(node.key, node);
})
遍历
after时,如果在和after中同时存在算法为 节点和after(键同) ,我们称之为 node 可重用。 // 更新前
const before = [
{key: 'a'},
{key: 'b'}
]
// 更新后
const after = [
{key: 'b'}
]
可复用的节点来说,本次更新必然属于以下两种情况之一: ❀未移动♝ 如何判断伙计,如果可重复使用的节点已被移动? ? 时间,每一轮穿过的节点必须是当前穿过的所有节点中最右边的一个。 节点是一个可复用节点,那么之前是相同的lastPlacedIndex 有两种关系: nodeBefore 表示对应的nodenodeBefore.index lastPlacedIndex对应的node。节点不在lastPlacedIndex对应的节点左侧,不是所有“当前遍历到的”)。 节点已移至右侧,必须标记为位置。 nodeBefore.index >= lastPlacedIndexNode 已就位,不得移动。 // 遍历到的最后一个可复用node在before中的index
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) {
// 存在可复用node
// 从map中剔除该 可复用node
beforeMap.delete(beforeNode.key);
const oldIndex = beforeNode.index as number;
// 核心判断逻辑
if (oldIndex < lastPlacedIndex) {
// 移动
afterNode.flag = 'Placement';
result.push(afterNode);
continue;
} else {
// 不移动
lastPlacedIndex = oldIndex;
}
} else {
// 不存在可复用node,这是一个新节点
afterNode.flag = 'Placement';
result.push(afterNode);
}
遍历结束工作
还剩下不可复用d 并且必须标记为删除。 节点,则代表❝无disse❝after后,ap中的中仍然有{key: 'a'}: // 更新前
const before = [
{key: 'a'},
{key: 'b'}
]
// 更新后
const after = [
{key: 'b'}
]
a 必须标记为删除。 beforeMap.forEach(node => {
node.flag = 'Deletion';
result.push(node);
});
总结 ♼❝ Diff
last放置索引相关逻辑。 Demo多调试几次。我想你能明白其中的原理。 参考资料
[1]在线演示地址:https://codesandbox.io/s/naughty-matan-6fq7n6?file=/src/diff.ts
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网