Vue 3 和 React 的虚拟 DOM 在核心概念上类似,但在实现细节、优化策略和使用方式上存在显著差异。以下是两者的关键对比:
一、核心实现差异
1. Diff 算法
-
Vue 3:
- 使用 预处理 + 最长递增子序列(LIS 算法,时间复杂度为 O(n log n)
- 优先处理相同前置 / 后置元素,快速跳过无需比较的节点
- 通过
ShapeFlag
位运算快速判断节点类型
-
React:
-
使用双指针遍历 + key 比较,默认时间复杂度为 O(n)
-
依赖
key
属性识别同层节点变化 -
2020 年后引入Fiber 架构,将渲染任务拆分为小单元(可中断)
-
javascript
// 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;
}
// 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.memo
、useMemo
、useCallback
等手动优化 - 需要开发者主动控制组件更新(如
shouldComponentUpdate
) - 引入Concurrent Mode(实验性)实现优先级渲染
- 运行时优化:依赖
二、实现策略差异
1. 模板 vs JSX
-
Vue 3:
-
主要使用模板语法(
.vue
文件) -
编译时生成优化的渲染函数
-
示例:
vue
<template> <div>{{ message }}</div> </template>
-
-
React:
-
主要使用JSX(JavaScript 语法扩展)
-
运行时编译 JSX 为
React.createElement
调用 -
示例:
jsx
function App() { return <div>{message}</div> }
-
2. 响应式系统
-
Vue 3:
-
内置Proxy-based 响应式系统
-
自动追踪依赖,精确触发更新
-
示例:
javascript
import { reactive } from 'vue' const state = reactive({ count: 0 }) // state.count变化时自动触发更新
-
-
React:
-
使用不可变数据和状态管理库(如 Redux)
-
通过
setState
或useState
显式触发更新 -
示例:
javascript
const [count, setCount] = useState(0) // 必须调用setCount才能触发更新
-
三、性能优化差异
1. 静态内容处理
-
Vue 3:
-
编译时识别静态节点并提升(Static Hoisting)
-
静态节点只创建一次,后续渲染直接复用
-
示例:
vue
<div> <h1>Static Title</h1> <!-- 编译时提升 --> <p>{{ dynamic }}</p> </div>
-
-
React:
- 没有编译时优化,静态节点每次渲染都会重新创建
- 需手动使用
React.memo
或提取组件避免重复渲染
2. 事件处理优化
-
Vue 3:
-
自动缓存事件处理函数(
cacheHandlers
) -
示例:
vue
<button @click="handleClick">Click</button> <!-- 编译后自动缓存handleClick -->
-
-
React:
-
需要手动使用
useCallback
缓存回调函数 -
示例:
jsx
const handleClick = useCallback(() => { // 处理逻辑 }, [dependencies])
-
四、架构设计差异
1. 组件更新粒度
-
Vue 3:
- 组件级更新:单个组件状态变化只会触发该组件重新渲染
- 基于响应式系统精确追踪依赖
-
React:
- 函数式组件默认全量重新渲染
- 需要通过
React.memo
、useMemo
等手动控制
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 / 函数式编程
- 需要与复杂状态管理库集成的场景