如何理解React的虚拟DOM?

真实的DOM

  在web应用中,浏览器接收到服务器返回的html文档后,会将html文档解析成DOM树,具体流程可以参考这篇博文。DOM树是浏览器在内存中对html文档的抽象化表示,方便开发者使用javascript操作html文档。在传统的前端开发模式下,界面的更新方式大致如图1所示。

图 1 传统界面更新方式

  传统的界面更新方式是利用javascript直接操作浏览器DOM树,这种做法对于结构简单的页面来说无可厚非,但是对于具有复杂交互功能的页面而言,其效率非常低下。实际上,对于一个大型网站而言,其DOM树结构非常复杂,使用javascript操作DOM树进行增、删、查、改等操作造成的开销很大,并且无法有效地减少无意义的DOM操作(比如使用javascript动态生成一个列表,就算新列表和旧列表之间只有一列不相同,但是新列表仍然会整个替换旧列表)。除了效率低下之外,传统的前端开发模式编写的代码非常不利于维护,代码中充斥着各种写死的类名和id名以及各种事件绑定函数。

虚拟DOM

  正因为DOM操作开销大,为了减少无意义的DOM操作,提高页面性能,React在浏览器DOM的基础上进行进一步抽象,实现了一套独立于浏览器的虚拟DOM。所谓虚拟DOM,即一个用来描述真实DOM的javascript对象。React使用ReactElement来对真实的DOM进行抽象,对于每一个真实的DOM元素,如div、span等,React均可以创建一个对应的ReactElement对象。React 16中创建ReactElement对象的createElement方法的源码及注释如下:

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
// 保留的属性,用户不能修改
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
/**
* 创建ReactElement对象的工厂函数
* @param {*} type 元素类型
* @param {*} key 元素的key,React的虚拟DOM diff算法会用到
* @param {string|object} ref ReactElemet生成的实际DOM的引用
* @param {*} self 临时变量,用于检测调用React.createElement时'this'与'owner'不同的地方,以便发出警告
* @param {*} source 一个注释对象,指明文件名、行号或者其他信息
* @param {*} owner 创建该元素的组件
* @param {*} props 元素属性
* @internal
*/
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// $$typeof属性可以用来判断元素类型,REACT_ELEMENT_TYPE是一个常量,表示当前对象为ReactElement类型
$$typeof: REACT_ELEMENT_TYPE,
// 类型
type: type,
// 标志
key: key,
// 生成的真实DOM的引用
ref: ref,
// 属性
props: props,
// 创建改元素的组件
_owner: owner,
};
// 开发环境下的操作
if (__DEV__) {
element._store = {};
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false,
});
// self属性和source属性只在开发环境下有用
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
// 冻结对象
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
/**
* @param {*} type 元素类型,取值可以是DOM元素名,如'div', 'span'等;可以取值为ReactComponent类型或 者ReactFragment类型
* @param {*} config 元素属性
* @param {*} children 元素子元素,取值可以为多个
*/
function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
// 判断输入的ref属性和key属性是否合法
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
// 设置self属性和source属性
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 将config中非保留属性复制到props中
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// 可以以多个参数的方式将子元素传入
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
// 只有一个子元素时,直接赋值给props对象的children属性
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 在开发环境下冻结childArray
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
// 拥有多个子元素时,转换成数组赋值给children属性
props.children = childArray;
}
// 解析默认属性
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 在开发环境下对key和ref进行处理
if (__DEV__) {
if (key || ref) {
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
// 试图直接访问key属性时出现警告
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
// 试图直接访问ref属性时出现警告
defineRefPropWarningGetter(props, displayName);
}
}
}
// 返回生成的ReactElement对象
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}

  在React中,所有对真实DOM的操作都转换为对虚拟DOM的操作。使用React开发时界面的更新方式可以用图2描述。

图 2 React界面更新方式

  在更新界面时,React不会直接操作所有ReactElement对象对应的真实DOM,而是首先对比当前新生成的ReactElement对象和旧ReactElement对象之间的差异,然后操作发生了变化的ReactElement对象对应的真实DOM来更新界面。React减少了无意义的DOM操作,提高了页面性能。
  在React中,ReactElement对象是不可变的,一旦被创建之后,其属性不能被更改,当需要更新界面时,必须重新创建和渲染ReactElement对象。为了更好地组织界面,React引入了ReactComponent的概念。ReactComponent是有状态的组件,当组件状态发生变化时,组件会自动创建新的ReactElement对象。本文重点在于介绍React的虚拟DOM,在后续文章中,作者会对ReactComponent进行详细的分析。

参考文献

  1. https://www.codecademy.com/articles/react-virtual-dom
  2. http://reactkungfu.com/2015/10/the-difference-between-virtual-dom-and-dom/
  3. https://reactjs.org/docs/dom-elements.html
  4. https://reactjs.org/docs/rendering-elements.html
  5. https://segmentfault.com/a/1190000010924023