Hello everyone, I'm Casson.
Any framework that relies on 虚拟DOM
needs to compare the Diff
algorithm that changes the nodes before and after.
There are a lot of articles on the Internet explaining Diff
algorithm logic. However, even if the author's language is more refined, and the pictures and texts are rich, I believe that most students will forget it after reading it for a long time.
Today, let's switch to a once-and-for-all learning method -- the core Diff
algorithm that implements React
.
Not difficult, only 40 lines of code. Do not believe? Look down.
Welcome to join the human high-quality front-end framework group , with flying
Design idea of Diff algorithm
Just imagine, Diff
how many situations does the algorithm need to consider? There are roughly three types:
- Node attribute changes, such as:
// 更新前
<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>
- Node additions and deletions, such as:
// 更新前
<ul>
<li key="0">0</li>
<li key="1">1</li>
<li key="2">2</li>
</ul>
// 更新后 情况1 —— 新增节点
<ul>
<li key="0">0</li>
<li key="1">1</li>
<li key="2">2</li>
<li key="3">3</li>
</ul>
// 更新后 情况2 —— 删除节点
<ul>
<li key="0">0</li>
<li key="1">1</li>
</ul>
- Node movement, such as:
// 更新前
<ul>
<li key="0">0</li>
<li key="1">1</li>
</ul>
// 更新后
<ul>
<li key="1">1</li>
<li key="0">0</li>
</ul>
How to design the Diff
algorithm? Considering only the above three situations, a common design idea is:
- First determine which situation the current node belongs to
- If it is an addition or deletion, execute the addition and deletion logic
- If it is an attribute change, execute the attribute change logic
- If it is a move, execute the move logic
According to this scheme, there is actually an implicit premise - the priority of different operations is the same . But in daily development, node movement happens less, so Diff
the algorithm will prioritize other cases.
Based on this concept, mainstream frameworks (React, Vue) Diff
algorithms will go through multiple rounds of traversal, first dealing with common situations , and then dealing with uncommon situations .
So, this requires that algorithms that handle uncommon cases need to be able to give all kinds of bounds case
the bottom line.
In other words, the Diff
operation can be done entirely using only algorithms that handle unusual cases . Mainstream frameworks do not do this for performance reasons.
This article will cut out the algorithms that deal with common cases and keep the algorithms that deal with uncommon cases .
In this way, only 40 lines of code are needed to implement the core logic of Diff
.
Demo introduction
First, we define the data structure of the 虚拟DOM
node:
type Flag = 'Placement' | 'Deletion';
interface Node {
key: string;
flag?: Flag;
index?: number;
}
key
is the unique identifier of node
, used to associate nodes before and after the change.
flag
represents node
after Diff
, the corresponding 真实DOM
operation needs to be performed, among which:
-
Placement
For the newly generatednode
, the correspondingDOM
need to be inserted into the page. For the existingnode
, the correspondingDOM
needs to be moved in the page -
Deletion
representsnode
corresponds toDOM
needs to be removed from the page
index
represents the index position of the node
in the same level node
Note: This Demo
is only implemented as a node flag flag , and does not implement DOM operations according to the flag .
We hope to achieve diff
method, receiving 更新前
and 更新后
of NodeList
, as they mark flag
:
type NodeList = Node[];
function diff(before: NodeList, after: NodeList): NodeList {
// ...代码
}
For example for:
// 更新前
const before = [
{key: 'a'}
]
// 更新后
const after = [
{key: 'd'}
]
// diff(before, after) 输出
[
{key: "d", flag: "Placement"},
{key: "a", flag: "Deletion"}
]
{key: "d", flag: "Placement"}
represents d corresponding to DOM
need to insert pages.
{key: "a", flag: "Deletion"}
represents a corresponding to DOM
needs to be deleted.
The result after execution is: a in the page becomes d.
Another example:
// 更新前
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"}
]
Since b
has existed before, {key: "b", flag: "Placement"}
represents b corresponding to DOM
need to move backwards (corresponding to parentNode.appendChild
abc
After this operation, it becomes acb
.
Since a
has existed before, {key: "a", flag: "Placement"}
represents a corresponding to DOM
needs to be moved backwards. acb
After this operation, it becomes cba
.
The result after execution is: abc in the page becomes cba.
Implementation of Diff Algorithm
The core logic consists of three steps:
- Preparation before traversal
- Traversal
after
- Finishing work after traversal
function diff(before: NodeList, after: NodeList): NodeList {
const result: NodeList = [];
// ...遍历前的准备工作
for (let i = 0; i < after.length; i++) {
// ...核心遍历逻辑
}
// ...遍历后的收尾工作
return result;
}
Preparation before traversal
We will before
each node
saved to node.key
to key
, node
as value
the Map
in.
In this way, the complexity of --- 67da8c8491ad6826558a6d9efe02511e O(1)
can be found by key
before
corresponding to node
:
// 保存结果
const result: NodeList = [];
// 将before保存在map中
const beforeMap = new Map<string, Node>();
before.forEach((node, i) => {
node.index = i;
beforeMap.set(node.key, node);
})
traverse after
When traversing after
when, if a node
exist in before
and after
( key
same), we Call this node
reusable.
For example, b is reusable for the following example:
// 更新前
const before = [
{key: 'a'},
{key: 'b'}
]
// 更新后
const after = [
{key: 'b'}
]
For 可复用的node
, this update must be one of the following two cases:
- do not move
- move
How to judge whether 可复用的node
has moved?
We use the lastPlacedIndex
variable to save the index of the last reusable node traversed to before :
// 遍历到的最后一个可复用node在before中的index
let lastPlacedIndex = 0;
When traversing after
, the node
traversed in each round must be the rightmost one of all the currently traversed node
.
If this node
is 可复用的node
, then nodeBefore
and lastPlacedIndex
have two relations:
Note:nodeBefore
represents the correspondingnode
可复用的node
inbefore
------3f247ec83d1d0d72ba90af0
-
nodeBefore.index < lastPlacedIndex
Represents the --- lastPlacedIndex对应node
node
before the update.
After the update, the node
is not on the left of lastPlacedIndex对应node
(because it is the rightmost one of all the nodes currently traversed ).
This means that the node
has moved to the right and needs to be marked Placement
.
-
nodeBefore.index >= lastPlacedIndex
The node
is in place, no need to move.
// 遍历到的最后一个可复用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);
}
Finishing work after traversal
After traversal, if beforeMap
there is still node
, it means that these node
cannot be reused and need to be marked and deleted.
For example, in the following case, after traversing after
, there is still --- beforeMap
in {key: 'a'}
:
// 更新前
const before = [
{key: 'a'},
{key: 'b'}
]
// 更新后
const after = [
{key: 'b'}
]
This means a
needs to be marked for deletion.
So, finally, you need to add the logic of marking deletion:
beforeMap.forEach(node => {
node.flag = 'Deletion';
result.push(node);
});
For the complete code, see the online Demo address
Summarize
The difficulty of the entire Diff
algorithm lies in the lastPlacedIndex
related logic.
Follow Demo
to debug several times, I believe you can understand the principle.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。