《Vue.js设计与实现》笔记 第9章:简单 Diff 算法

Vue.js设计与实现 第9章:简单 Diff 算法

本章导读

前面第8章已经实现:

Element节点

创建
更新
删除

但是:

oldChildren = [
  A,
  B,
  C
]

newChildren = [
  B,
  C,
  D
]

这种情况无法高效处理。

如果直接:

删除全部旧节点

重新创建全部新节点

虽然正确:

性能极差

因此需要:

Diff算法

目的:

尽可能复用已有DOM

一、什么是Diff算法

定义

Diff:

Difference
差异比较

作用:

比较两棵VNode树

找出变化部分

最小化DOM操作

示例

旧节点:

[
  A,
  B,
  C
]

新节点:

[
  A,
  B,
  D
]

理想结果:

复用A

复用B

删除C

新增D

而不是:

全部删除

全部重建

二、为什么需要Key

没有Key

旧:

<p>A</p>
<p>B</p>
<p>C</p>

新:

<p>B</p>
<p>C</p>
<p>D</p>

Vue只能:

按索引比较

结果:

A → B

B → C

C → D

全部更新。


有Key

旧:

[
 {key:1},
 {key:2},
 {key:3}
]

新:

[
 {key:2},
 {key:3},
 {key:4}
]

可以准确知道:

1 删除

2 复用

3 复用

4 新增

三、Key的作用

Key本质:

VNode唯一标识

Diff时:

oldVNode.key
===
newVNode.key

表示:

同一个节点

因此:

可以复用DOM

四、简单Diff核心思想

遍历新节点

for(
 const newVNode
 of newChildren
)

在旧节点中查找

for(
 const oldVNode
 of oldChildren
)

判断:

oldVNode.key
===
newVNode.key

找到:

复用节点

找不到:

新增节点

五、DOM复用

示例

旧:

[
 {key:1},
 {key:2},
 {key:3}
]

新:

[
 {key:2},
 {key:3}
]

流程:

找到2

patch更新

找到3

patch更新

实现:

patch(
 oldVNode,
 newVNode
)

复用:

newVNode.el =
 oldVNode.el

六、判断节点移动

示例

旧:

[
 1,
 2,
 3
]

新:

[
 3,
 1,
 2
]

节点:

全部存在

但是:

位置改变

需要:

移动DOM

七、寻找是否需要移动

引入lastIndex

记录:

已处理节点中

最大的旧索引

初始化:

let lastIndex = 0

八、移动判断原理

示例

旧:

A B C D

0 1 2 3

新:

D A B C

处理:

D
旧索引=3

lastIndex=3

继续:

A
旧索引=0

发现:

0 < 3

说明:

A发生移动

九、移动规则

如果:

oldIndex
<
lastIndex

说明:

当前节点需要移动

否则:

lastIndex = oldIndex

表示:

当前位置正常

十、如何移动DOM

找锚点

例如:

insert(
 el,
 container,
 anchor
)

anchor:

插入位置参考节点

效果:

把DOM移动到正确位置

十一、新增节点处理

示例

旧:

[
 1,
 2
]

新:

[
 1,
 2,
 3
]

查找:

3不存在

处理:

创建新节点

执行:

patch(
 null,
 newVNode
)

十二、确定新增位置

示例

新:

[
 1,
 2,
 3
]

新增:

3

锚点:

下一个节点

如果:

没有下一个节点

则:

appendChild

十三、删除节点处理

示例

旧:

[
 1,
 2,
 3
]

新:

[
 1,
 2
]

遍历旧节点:

oldChildren

发现:

3不存在

执行:

unmount()

删除DOM。


十四、完整Diff流程

第一步

遍历:

newChildren

第二步

寻找:

key相同节点

找到:

patch更新

找不到:

新增

第三步

判断:

是否移动

第四步

遍历:

oldChildren

不存在于:

newChildren

执行:

删除

十五、简单Diff时间复杂度

外层循环

newChildren

内层循环

oldChildren

复杂度:

O(n²)

为什么慢

例如:

1000节点

1000 × 1000

=
100万次比较

性能较差。


十六、简单Diff优缺点

优点

实现简单。

容易理解。


缺点

时间复杂度:

O(n²)

大量节点:

性能差

因此:

Vue2最终采用:

双端Diff

Vue3最终采用:

快速Diff

十七、Vue中的节点类型

Diff过程中比较:

type

以及:

key

同时满足:

type相同

key相同

才能:

复用DOM

否则:

卸载

重新创建

十八、为什么Key不能使用Index

错误示例

<div
 v-for="item in list"
 :key="index"
/>

插入头部:

A B C

↓

D A B C

结果:

全部错位

大量DOM更新。


正确方式

:key="item.id"

保证:

稳定且唯一

第9章核心知识图谱

Simple Diff

oldChildren
       │
       ▼
newChildren

       │

       ▼

寻找相同key

       │

 ┌─────┼─────┐

 ▼     ▼     ▼

复用   新增   删除

 │

 ▼

移动判断

 │

 ▼

oldIndex

↓

lastIndex

↓

DOM Move

高频面试题

Diff算法作用是什么?

比较:

新旧VNode

找出:

  • 新增
  • 删除
  • 更新
  • 移动

为什么需要Key?

唯一标识VNode。

帮助DOM复用。


Key和Type都要比较吗?

需要。

满足:

type相同

key相同

才能复用。


简单Diff时间复杂度是多少?

O(n²)

lastIndex作用是什么?

判断:

节点是否需要移动

为什么不推荐Index作为Key?

列表变化时:

节点错位

大量更新

新增节点如何处理?

patch(
 null,
 vnode
)

删除节点如何处理?

unmount(vnode)

本章总结

简单Diff算法核心思想:

遍历新节点

↓

寻找可复用节点

↓

更新

↓

移动

↓

新增

↓

删除

判断节点复用:

type相同

+

key相同

判断移动:

oldIndex < lastIndex

特点:

实现简单

但复杂度O(n²)

因此后续:

第10章
双端Diff

↓

第11章
快速Diff

将进一步优化性能。

简单Diff是理解所有Diff算法的基础,也是面试中经常考察的核心知识点。

posted @ 2025-05-15 10:55  Li_pk  阅读(3)  评论(0)    收藏  举报