0%

Vue 与 React 虚拟 DOM 对比——渲染机制差异分析

今天天气转凉,就像 diff 算法一样,要去掉多余的部分,保留精华。让我们来聊聊 Vue 和 React 的虚拟 DOM 和 Diff 算法…

介绍

  虚拟 DOM(Virtual DOM)是现代前端框架的核心概念之一,它是一种编程概念,即虚拟表现形式(内存中的数据结构)与真实 DOM 进行同步。这种技术允许框架在不直接操作 DOM 的情况下更新页面,从而提高性能和用户体验。

  在 Vue 和 React 这样的现代前端框架中,虚拟 DOM 和 Diff 算法扮演着至关重要的角色,它们使得复杂的 UI 更新变得更加高效和可控。

虚拟 DOM 的基本概念

什么是虚拟 DOM?

  虚拟 DOM 是一个轻量级的 Javascript 对象,它代表了真实 DOM 的结构。这个对象包含了标签名、属性、子节点等信息,但不包含任何浏览器特定的方法和属性。

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
// 一个简单的虚拟 DOM 节点示例 const vnode = {
tag: 'div',
props: {
className: 'container',
id: 'app'
},
children: [
{
tag: 'h1',
props: {},
children: ['Hello Virtual DOM']
},
{
tag: 'p',
props: { style: { color: 'red' } },
children: ['这是一个段落']
}
]
};

// 真实 DOM 结构
/*
<div class="container" id="app">
<h1>Hello Virtual DOM</h1>
<p style="color:red">这是一个段落</p>
</div>
*/

虚拟 DOM 的工作流程

  1. 创建阶段:将真实的 DOM 结构转换为 Javascript 对象(虚拟 DOM)
  2. 更新阶段:当数据变化时,生成新的虚拟 DOM 树
  3. 比较阶段:使用 Diff 算法比较新旧虚拟 DOM 树的差异
  4. 应用阶段:将差异应用到真实 DOM 上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 虚拟 DOM 的核心工作流程 function virtualDOMWorkflow() {
// 1. 创建初始虚拟 DOM
const oldVNode = createElement('div', { id: 'app' }, [
createElement('h1', {}, ['Initial Content'])
]);

// 2. 数据变化后创建新的虚拟 DOM
const newVNode = createElement('div', { id: 'app' }, [
createElement('h1', {}, ['Updated Content']),
createElement('p', {}, ['New Paragraph'])
]);

// 3. 比较差异 const patches = diff(oldVNode, newVNode);

// 4. 应用差异到真实 DOM
patch(realDOM, patches);
}

Vue 的虚拟 DOM 实现

Vue2.x 的虚拟 DOM

  Vue2.x 使用了一个相对简单的虚拟 DOM 实现,基于 snabbdom 库的思想。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Vue2.x 虚拟 DOM 节点结构 function VNode(tag, data, children, text, elm) {
this.tag = tag; // 标签名 this.data = data; // 属性数据 this.children = children; // 子节点 this.text = text; // 文本节点内容 this.elm = elm; // 对应的真实 DOM 节点 this.key = data && data.key; // 节点的唯一标识
}

// Vue2.x 中的 createElement 函数 function createElement(context, tag, data, children) {
// ... 简化版实现 return new VNode(
tag,
data,
normalizeChildren(children),
undefined,
undefined
);
}

Vue3.x 的虚拟 DOM 改进

  Vue3.x 引入了编译时优化,生成 Block Tree,大大提升了 Diff 算法的性能。

1
2
3
4
5
6
7
8
9
10
11
12
// Vue3.x 的优化策略
// 静态节点提升 - 避免重复创建静态节点 const _hoisted_1 = /*#__PURE__*/createElementVNode("div", { class: "static" }, "Static Content")

// 静态属性提升 const _hoisted_2 = { class: "static-class" }

// 动态节点跟踪 - 只追踪可能变化的部分 export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (openBlock(), createElementBlock("div", null, [
// 静态节点不会被追踪
_hoisted_1,
// 动态节点会被追踪 createElementVNode("span", null, _toDisplayString(_ctx.dynamicValue), 1 /* TEXT */)
]))
}

Vue 的 Diff 算法实现

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
// Vue 的 Diff 算法核心 - 同层级比较 function patch(oldVnode, vnode, hydrating, removeOnly) {
if (oldVnode === vnode) {
return
}

// 静态节点优化 if (isDef(vnode) && isTrue(vnode.isStatic) &&
isDef(oldVnode) && isTrue(oldVnode.isStatic)) {
vnode.elm = oldVnode.elm
vnode.componentInstance = oldVnode.componentInstance
return
}

const elm = vnode.elm = oldVnode.elm

// 检查是否为相同节点 if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
// 不是相同节点,直接替换 const parentElm = nodeOps.parentNode(oldVnode.elm)
createElm(vnode, insertedVnodeQueue)
if (parentElm !== null) {
nodeOps.insertBefore(parentElm, vnode.elm, nodeOps.nextSibling(oldVnode.elm))
removeVnodes(parentElm, [oldVnode], 0, 0)
}
}
}

// 判断是否为相同节点 function sameVnode(a, b) {
return (
a.key === b.key && // key 相同 a.asyncFactory === b.asyncFactory && // 异步工厂相同
(a.tag === b.tag && a.isComment === b.isComment && // 标签相同 isDef(a.data) === isDef(b.data) && // 数据存在性相同 sameInputType(a, b)) // input 类型相同
)
}

// Patch 核心逻辑 function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 更新节点数据 if (isDef(data) && isPatchable(vnode)) {
for (let i = 0; i < cbs.update.length; ++i) {
cbs.update[i](oldVnode, vnode)
}
const hooks = vnode.data.hook
if (isDef(hooks) && isDef(i = hooks.prepatch)) {
i(oldVnode, vnode)
}
}

// 更新子节点 const oldCh = oldVnode.children
const ch = vnode.children

if (isDef(data) && isPatchable(vnode)) {
for (let i = 0; i < cbs.update.length; ++i) {
cbs.update[i](oldVnode, vnode)
}
const hooks = vnode.data.hook
if (isDef(hooks) && isDef(i = hooks.prepatch)) {
i(oldVnode, vnode)
}
}

// 子节点处理逻辑 if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}

if (isDef(hooks) && isDef(i = hooks.update)) i(oldVnode, vnode)
}

React 的虚拟 DOM 实现

React Element 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// React.createElement 返回的虚拟 DOM 结构 const element = {
$$typeof: Symbol.for('React.element'),
type: 'div', // 标签名或组件构造函数 key: null, // 唯一标识 ref: null, // 引用 props: { // 属性 className: 'container',
children: [
{
$$typeof: Symbol.for('React.element'),
type: 'h1',
key: null,
ref: null,
props: { children: 'Hello World' },
_owner: null
}
]
},
_owner: null // 组件所有者
};

React Fiber 架构

  React16引入了 Fiber 架构,这是一个增量渲染的实现,将渲染工作分解为多个小块,可以在必要时暂停、终止、复用渲染工作。

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
// React Fiber 节点结构 function FiberNode(
tag, // 节点类型 pendingProps, // 待处理的属性 key, // 唯一键 mode // 模式
) {
// 基础信息 this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;

// Fiber 树结构 this.return = null; // 父节点 this.child = null; // 子节点 this.sibling = null; // 兄弟节点 this.index = 0;

// 替代节点(用于协调)
this.alternate = null;

// 优先级相关 this.mode = mode;
this.lanes = NoLanes;
this.childLanes = NoLanes;

// 副作用相关 this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;

// 更新队列 this.memoizedState = null;
this.updateQueue = null;
}

React 的 Diff 算法优化

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
209
210
211
212
213
214
215
216
217
// React 的 Diff 算法优化策略 function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;

if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}

// 处理不同类型的新节点 if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
// ... 其他类型处理
}
}

if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}

// 文本节点处理 if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}

// 处理文本节点 if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}

return deleteRemainingChildren(returnFiber, currentFirstChild);
}

// 单个元素协调 function reconcileSingleElement(
returnFiber,
currentFirstChild,
element,
lanes,
) {
const key = element.key;
let child = currentFirstChild;

// 如果有 key,寻找匹配的节点 while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}
} else {
if (child.elementType === elementType) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
}
// 不匹配则删除 deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}

// 创建新节点 const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}

// 数组协调(重点:key 优化)
function reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChildren,
lanes,
) {
let resultingFirstChild = null;
let previousNewFiber = null;

let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;

// 第一轮:新旧节点都有的情况下,使用 key 进行匹配 for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}

const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);

if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}

if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}

lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}

// 新节点处理完毕 if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}

// 旧节点处理完毕 if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}

// 剩余旧节点创建 Map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);

if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}

lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}

if (shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}

return resultingFirstChild;
}

Vue 与 React Diff 算法对比

时间复杂度对比

特性VueReact
基础复杂度O(n)O(n)
Key 优化O(n)O(n)
静态提升Vue3: O(1)React: O(n)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Vue3 静态节点优化示例
// 编译前 const app = {
render() {
return [
h('div', { class: 'static' }, 'This never changes'),
h('span', {}, this.dynamicValue)
]
}
}

// 编译后 - 静态节点被提升 const _hoisted_1 = h('div', { class: 'static' }, 'This never changes')

const app = {
render() {
return [
_hoisted_1,
h('span', {}, this.dynamicValue)
]
}
}

Key 的作用和最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 错误的 key 使用 - 使用数组索引 const WrongList = ({ items }) => (
<div>
{items.map((item, index) => (
<div key={index}>{item.name}</div> // 错误:使用 index 作为 key
))}
</div>
);

// 正确的 key 使用 - 使用唯一 ID
const CorrectList = ({ items }) => (
<div>
{items.map(item => (
<div key={item.id}>{item.name}</div> // 正确:使用唯一 id
))}
</div>
);

// Vue 中同样需要注意 key 的使用 const VueList = {
template: `
<div>
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
</div>
`
}

性能优化策略

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
// Vue 的性能优化
// 1. 使用 v-memo(Vue3.2+)
<template>
<div v-memo="[record.id, record.selected]">
{{ record.title }}
</div>
</template>

// 2. 使用 keep-alive 缓存组件
<keep-alive>
<component :is="currentView"></component>
</keep-alive>

// 3. 使用 v-show 代替频繁切换的 v-if
<template>
<!-- 频繁切换用 v-show -->
<div v-show="isVisible">内容</div>

<!-- 首次渲染条件用 v-if -->
<div v-if="shouldRender">内容</div>
</template>

// React 的性能优化
// 1. 使用 React.memo
const MemoComponent = React.memo(({ value }) => {
return <div>{value}</div>;
});

// 2. 使用 useMemo 和 useCallback
const ExpensiveComponent = ({ items, filter }) => {
const filteredItems = useMemo(() => {
return items.filter(item => item.category === filter);
}, [items, filter]);

const handleItemClick = useCallback((id) => {
console.log('Item clicked:', id);
}, []);

return (
<div>
{filteredItems.map(item => (
<Item key={item.id} item={item} onClick={handleItemClick} />
))}
</div>
);
};

// 3. 使用 React.lazy 和 Suspense
const LazyComponent = React.lazy(() => import('./LazyComponent'));

const App = () => (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);

实际应用案例

列表渲染优化

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
// Vue - 列表渲染最佳实践
<template>
<ul>
<!-- 使用唯一的 key -->
<li
v-for="item in items"
:key="item.id"
:class="{ active: item.active }"
>
{{ item.name }}
<!-- 避免在 v-for 中使用复杂表达式 -->
<span :class="getStatusClass(item.status)">
{{ getStatusText(item.status) }}
</span>
</li>
</ul>
</template>

<script>
export default {
computed: {
// 计算属性避免重复计算 processedItems() {
return this.items.map(item => ({
...item,
processed: true
}));
}
},
methods: {
// 方法缓存 getStatusClass(status) {
return `status-${status}`;
},
getStatusText(status) {
return status === 'active' ? '活跃' : '非活跃';
}
}
}
</script>

// React - 列表渲染最佳实践 const ItemList = ({ items, onItemClick }) => {
// 使用 memo 包装列表项组件 const ListItem = React.memo(({ item, onClick }) => {
return (
<li
className={`item ${item.active ? 'active' : ''}`}
onClick={() => onClick(item.id)}
>
<span className={`status-${item.status}`}>
{item.name}
</span>
</li>
);
});

return (
<ul>
{items.map(item => (
<ListItem
key={item.id}
item={item}
onClick={onItemClick}
/>
))}
</ul>
);
};

组件通信优化

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
// Vue - 优化组件间通信
// 使用 provide/inject 减少 props 传递层级 const Parent = {
provide() {
return {
sharedData: this.sharedData,
updateSharedData: this.updateSharedData
};
},
data() {
return {
sharedData: {}
};
},
methods: {
updateSharedData(newData) {
this.sharedData = newData;
}
}
};

const DeepChild = {
inject: ['sharedData', 'updateSharedData'],
mounted() {
console.log(this.sharedData);
}
};

// React - 使用 Context 优化数据传递 const DataContext = React.createContext();

const DataProvider = ({ children, initialData }) => {
const [data, setData] = React.useState(initialData);

const value = React.useMemo(() => ({
data,
updateData: setData
}), [data]);

return (
<DataContext.Provider value={value}>
{children}
</DataContext.Provider>
);
};

const DeepComponent = () => {
const { data, updateData } = React.useContext(DataContext);

return (
<div>
{/* 组件使用数据 */}
</div>
);
};

调试和性能分析

Vue 调试工具

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
// 使用 Vue DevTools 调试虚拟 DOM
// 1. 安装 Vue DevTools 浏览器扩展
// 2. 在代码中启用组件名称 export default {
name: 'MyComponent', // 便于在 DevTools 中识别 data() {
return {
count: 0
};
},
computed: {
doubledCount() {
return this.count * 2;
}
}
};

// 使用 Vue 的 performance API
if (process.env.NODE_ENV !== 'production') {
performance.mark('start-render');
}

// 组件渲染逻辑...

if (process.env.NODE_ENV !== 'production') {
performance.mark('end-render');
performance.measure('render-time', 'start-render', 'end-render');
}

React Profiler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Profiler } from 'React';

const onRenderCallback = (
id, // 发生提交的 Profiler 树的 "id"
phase, // "mount" (如果组件树刚加载) 或 "update" (如果它重渲染了)
actualDuration, // 本次更新 committed 花费的渲染时间 baseDuration, // 估计不使用 memoization 的情况下渲染整颗子树需要的时间 startTime, // 本次更新中 React 开始渲染的时间 commitTime, // 本次更新中 React committed 的时间 interactions // 属于本次更新的 traces 的集合
) => {
console.log({
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
interactions
});
};

const App = () => (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
);

总结

  • 虚拟 DOM 是现代前端框架的核心概念,通过在内存中操作 DOM 树来提升性能
  • Vue 的 Diff 算法在 Vue2.x 基础上,Vue3.x 通过编译时优化大幅提升性能
  • React 通过 Fiber 架构实现了增量渲染,提高了大型应用的响应性
  • Key 的正确使用对列表渲染性能至关重要,应该使用稳定、唯一、可预测的值
  • 两个框架都有各自的优化策略,关键在于理解其工作原理并合理应用
  • 性能优化不仅依赖框架本身的优化,也需要开发者遵循最佳实践

今天写这篇文章让我体会到,就像天气转凉一样,虚拟 DOM 技术也”去掉”了不必要的 DOM 操作,”保留”了真正需要更新的部分,这就是技术与生活的奇妙呼应!

深入阅读

  1. Vue.JS 源码分析
  2. React Fiber 架构详解
  3. Virtual DOM and Internals
  4. React Reconciliation

练习建议

  1. 尝试手写一个简化版的虚拟 DOM 实现
  2. 分析不同 key 策略对列表渲染性能的影响
  3. 使用性能分析工具比较优化前后的差异
  4. 阅读 Vue 和 React 的源码,深入理解其实现细节
bulb