23
头图

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:

  1. 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>
  1. 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>
  1. 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:

  1. First determine which situation the current node belongs to
  2. If it is an addition or deletion, execute the addition and deletion logic
  3. If it is an attribute change, execute the attribute change logic
  4. 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 generated node , the corresponding DOM need to be inserted into the page. For the existing node , the corresponding DOM needs to be moved in the page
  • Deletion represents node corresponds to DOM 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:

  1. Preparation before traversal
  2. Traversal after
  3. 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 corresponding node 可复用的node in before ------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.


卡颂
3.1k 声望16.7k 粉丝