vue3 和 react 虚拟dom

Vue 3 和 React 的虚拟 DOM 在核心概念上类似,但在实现细节、优化策略和使用方式上存在显著差异。以下是两者的关键对比:

一、核心实现差异

1. Diff 算法

  • Vue 3

    • 使用 预处理 + 最长递增子序列(LIS 算法,时间复杂度为 O(n log n)
    • 优先处理相同前置 / 后置元素,快速跳过无需比较的节点
    • 通过ShapeFlag位运算快速判断节点类型
  • React

    • 使用双指针遍历 + key 比较,默认时间复杂度为 O(n)
    • 依赖key属性识别同层节点变化
    • 2020 年后引入Fiber 架构,将渲染任务拆分为小单元(可中断)

javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// Vue 3的Diff算法(简化)
// 1. 预处理相同前置/后置节点
// 2. 处理新增/删除节点
// 3. 使用LIS算法计算最小移动次数
function patchKeyedChildren(c1, c2, container, parentAnchor) {
let i = 0;
const l1 = c1.length;
const l2 = c2.length;
let e1 = l1 - 1; // 旧节点的结束索引
let e2 = l2 - 1; // 新节点的结束索引

// 1. 预处理相同前置节点
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i];
const n2 = c2[i];
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container);
} else {
break;
}
i++;
}

// 2. 预处理相同后置节点
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
const n1 = c1[e1];
const n2 = c2[e2];
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container);
} else {
break;
}
e1--;
e2--;
}

// 3. 处理新增节点(旧节点已遍历完,新节点有剩余)
// (a b)
// (a b) c d
// i = 2, e1 = 1, e2 = 3
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
while (i <= e2) {
patch(null, c2[i], container, anchor);
i++;
}
}
}

// 4. 处理删除节点(新节点已遍历完,旧节点有剩余)
// (a b) c d
// (a b)
// i = 2, e1 = 3, e2 = 1
else if (i > e2) {
while (i <= e1) {
unmount(c1[i]);
i++;
}
}

// 5. 处理乱序节点(核心Diff)
// a b [c d e] f g
// a b [e d c h] f g
else {
const s1 = i; // 旧节点的开始索引
const s2 = i; // 新节点的开始索引

// 5.1 建立新节点的key到index的映射
const keyToNewIndexMap = new Map();
for (i = s2; i <= e2; i++) {
const nextChild = c2[i];
if (nextChild.key !== null) {
keyToNewIndexMap.set(nextChild.key, i);
}
}

// 5.2 遍历旧节点,寻找匹配的新节点
let j;
let patched = 0;
const toBePatched = e2 - s2 + 1;
let moved = false;
let maxNewIndexSoFar = 0;
const newIndexToOldIndexMap = new Array(toBePatched).fill(0);

for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
if (patched >= toBePatched) {
// 所有新节点都已处理,剩余旧节点全部删除
unmount(prevChild);
continue;
}

let newIndex;
if (prevChild.key !== null) {
// 通过key查找新节点位置
newIndex = keyToNewIndexMap.get(prevChild.key);
} else {
// 没有key,遍历查找
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j])
) {
newIndex = j;
break;
}
}
}

if (newIndex === undefined) {
// 没有找到匹配的新节点,删除当前旧节点
unmount(prevChild);
} else {
// 保存旧节点索引(+1 是为了避免与默认值0冲突)
newIndexToOldIndexMap[newIndex - s2] = i + 1;

// 判断节点是否需要移动
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true;
}

// 复用旧节点,更新内容
patch(prevChild, c2[newIndex], container);
patched++;
}
}

// 5.3 使用LIS算法计算最小移动次数
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: [];
j = increasingNewIndexSequence.length - 1;

// 5.4 移动和插入节点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;

if (newIndexToOldIndexMap[i] === 0) {
// 新节点,需要插入
patch(null, nextChild, container, anchor);
} else if (moved) {
// 需要移动节点
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor);
} else {
j--;
}
}
}
}
}

// 判断两个VNode是否可以复用(key和type都相同)
function isSameVNodeType(n1, n2) {
return n1.type === n2.type && n1.key === n2.key;
}

// 最长递增子序列算法(Vue 3源码实现)
function getSequence(arr) {
const p = arr.slice();
const result = [0];
let i, j, u, v, c;
const len = arr.length;
for (i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1];
if (arr[j] < arrI) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// React的Diff算法(简化)
// 1. 双指针遍历新旧children
// 2. 根据key判断节点是更新、新增还是删除
function reconcileChildren(current, workInProgress) {
// 当前渲染的fiber节点
const currentFirstChild = current.child;
// 工作中的fiber节点(新的虚拟DOM)
let workInProgressChild = workInProgress.child;

// 1. 双指针初始化
let oldFiber = currentFirstChild;
let newIdx = 0;
let prevNewFiber = null;

// 2. 第一轮遍历:处理相同位置的节点(快速路径)
while (oldFiber !== null && newIdx < newChildren.length) {
const newChild = newChildren[newIdx];

// 2.1 通过key和type判断节点是否可以复用
const sameType = oldFiber.type === newChild.type;

if (!sameType) {
// 2.2 类型不同,无法复用,标记旧节点为删除
if (oldFiber) {
deleteChild(workInProgress, oldFiber);
}
break;
}

// 2.3 可以复用,创建新的fiber节点
const newFiber = createWorkInProgress(oldFiber, newChild.props);

// 2.4 连接到DOM树
if (prevNewFiber === null) {
workInProgressChild = newFiber;
} else {
prevNewFiber.sibling = newFiber;
}
prevNewFiber = newFiber;

// 2.5 移动指针
oldFiber = oldFiber.sibling;
newIdx++;
}

// 3. 处理新增节点(旧列表已遍历完,新列表还有剩余)
if (oldFiber === null) {
while (newIdx < newChildren.length) {
const newChild = newChildren[newIdx];
const newFiber = createFiberFromElement(newChild);

if (prevNewFiber === null) {
workInProgressChild = newFiber;
} else {
prevNewFiber.sibling = newFiber;
}
prevNewFiber = newFiber;
newIdx++;
}
return;
}

// 4. 处理删除节点(新列表已遍历完,旧列表还有剩余)
if (newIdx === newChildren.length) {
while (oldFiber !== null) {
deleteChild(workInProgress, oldFiber);
oldFiber = oldFiber.sibling;
}
return;
}

// 5. 复杂情况:乱序节点处理(使用key进行重排序)
// 5.1 建立旧节点的key到index的映射
const existingChildren = mapRemainingChildren(oldFiber);

// 5.2 遍历剩余新节点,寻找最佳匹配
for (; newIdx < newChildren.length; newIdx++) {
const newChild = newChildren[newIdx];

// 5.3 通过key查找可复用的旧节点
const matchedFiber = existingChildren.get(
newChild.key === null ? newChild.type : newChild.key
);

if (matchedFiber) {
// 5.4 复用找到的节点
const newFiber = useFiber(matchedFiber, newChild.props);
// 从映射中删除已复用的节点
existingChildren.delete(
newChild.key === null ? newChild.type : newChild.key
);

// 连接到DOM树
if (prevNewFiber === null) {
workInProgressChild = newFiber;
} else {
prevNewFiber.sibling = newFiber;
}
prevNewFiber = newFiber;
} else {
// 5.5 没有可复用的节点,创建新节点
const newFiber = createFiberFromElement(newChild);

if (prevNewFiber === null) {
workInProgressChild = newFiber;
} else {
prevNewFiber.sibling = newFiber;
}
prevNewFiber = newFiber;
}
}

// 5.6 删除所有未被复用的旧节点
existingChildren.forEach(child => deleteChild(workInProgress, child));
}

// 辅助函数:建立旧节点的key到fiber的映射
function mapRemainingChildren(currentFirstChild) {
const existingChildren = new Map();
let existingChild = currentFirstChild;

while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.type, existingChild);
}
existingChild = existingChild.sibling;
}

return existingChildren;
}

2. 渲染优化

  • Vue 3

    • 编译时优化:静态提升(Static Hoisting)、Block Tree
    • 自动标记动态节点,减少 Diff 范围
    • 事件处理函数缓存(cacheHandlers
  • React

    • 运行时优化:依赖React.memouseMemouseCallback等手动优化
    • 需要开发者主动控制组件更新(如shouldComponentUpdate
    • 引入Concurrent Mode(实验性)实现优先级渲染

二、实现策略差异

1. 模板 vs JSX

  • Vue 3

    • 主要使用模板语法.vue文件)

    • 编译时生成优化的渲染函数

    • 示例:

      vue

      1
      2
      3
      <template>
      <div>{{ message }}</div>
      </template>
  • React

    • 主要使用JSX(JavaScript 语法扩展)

    • 运行时编译 JSX 为React.createElement调用

    • 示例:

      jsx

      1
      2
      3
      function App() {
      return <div>{message}</div>
      }

2. 响应式系统

  • Vue 3

    • 内置Proxy-based 响应式系统

    • 自动追踪依赖,精确触发更新

    • 示例:

      javascript

      1
      2
      3
      import { reactive } from 'vue'
      const state = reactive({ count: 0 })
      // state.count变化时自动触发更新
  • React

    • 使用不可变数据状态管理库(如 Redux)

    • 通过setStateuseState显式触发更新

    • 示例:

      javascript

      1
      2
      const [count, setCount] = useState(0)
      // 必须调用setCount才能触发更新

三、性能优化差异

1. 静态内容处理

  • Vue 3

    • 编译时识别静态节点并提升(Static Hoisting)

    • 静态节点只创建一次,后续渲染直接复用

    • 示例:

      vue

      1
      2
      3
      4
      <div>
      <h1>Static Title</h1> <!-- 编译时提升 -->
      <p>{{ dynamic }}</p>
      </div>
  • React

    • 没有编译时优化,静态节点每次渲染都会重新创建
    • 需手动使用React.memo或提取组件避免重复渲染

2. 事件处理优化

  • Vue 3

    • 自动缓存事件处理函数(cacheHandlers

    • 示例:

      vue

      1
      2
      <button @click="handleClick">Click</button>
      <!-- 编译后自动缓存handleClick -->
  • React

    • 需要手动使用useCallback缓存回调函数

    • 示例:

      jsx

      1
      2
      3
      const handleClick = useCallback(() => {
      // 处理逻辑
      }, [dependencies])

四、架构设计差异

1. 组件更新粒度

  • Vue 3

    • 组件级更新:单个组件状态变化只会触发该组件重新渲染
    • 基于响应式系统精确追踪依赖
  • React

    • 函数式组件默认全量重新渲染
    • 需要通过React.memouseMemo等手动控制

2. 异步渲染

  • Vue 3

    • 渲染过程是同步的,但更新是异步批量
    • 通过nextTick访问更新后的 DOM
  • React

    • Concurrent Mode(实验性)支持异步渲染
    • 可中断渲染过程,优先处理高优先级任务

五、总结对比表

特性 Vue 3 React
Diff 算法 预处理 + LIS(O (n log n)) 双指针 + key(O (n))
优化方式 编译时自动优化(静态提升、Block) 运行时手动优化(React.memo)
响应式系统 内置 Proxy-based 响应式 不可变数据 + 显式 setState
模板语法 声明式模板 JSX
更新粒度 组件级 函数式组件默认全量更新
异步渲染 异步批量更新 Concurrent Mode(实验性)

六、适用场景

  • Vue 3

    • 适合需要高性能渲染的大型应用
    • 对开发效率有较高要求(编译时优化减少手动工作)
    • 偏好声明式模板语法的开发者
  • React

    • 适合需要灵活控制渲染过程的应用
    • 团队熟悉 JavaScript / 函数式编程
    • 需要与复杂状态管理库集成的场景
文章作者: Sir_Liu
文章链接: https://gofugui.github.io/2025/05/22/vue3 和 react 虚拟dom/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Coding Your Life