虚拟DOM和DOM的不同

原文:The difference between Virtual DOM and DOM - Bartosz Krajka

概述

React直接在首页上标榜虚拟DOM,这个特性应该很厉害。

那么问题来了,『虚拟DOM』到底是个什么鬼?

DOM

先直接说一下,DOM意思是文档对象模型(Dcoument Object Model),它是一个结构化文本的抽象。对于Web开发者,这个文本是一段HTML代码,DOM也就被叫做HTML DOM。HTML的元素在DOM中变成了节点。

所以,HTML是一段文本,DOM就是这段文本在内存中的表示。

可以对比一个程序的一个进程实例。对于一个程序,可以存在多个进程,就像一段同样的HTML可以有多个DOM一样。(例:同一个页面被多个tab加载)。

在HTML DOM中提供了遍历和修改节点的接口(API)。像getElementById或者removeChild这样的方法。我们一般使用JavaScript来操作DOM,这是因为……好吧,天知道为什么。:)

所以,只要我们想要动态修改网页的内容的时候,我们就修改DOM。

1
2
var item = document.getElementById("myLI");
item.parentNode.removeChild(item);

所谓document就是根节点的抽象,而getElementByIdparentNodeemoveChild则是HTML DOM API中的方法。

问题

HTML DOM永远都是HTML文档结构所允许的树结构。树结构很不错,我们可以相当容易地遍历树。但是悲剧地是,容易遍历不代表快速遍历。

这个年代的DOM树都很巨大,既然我们越来越被推向动态Web应用(单页应用 - SPA),我们需要连续不断地大量地修改DOM树,这个确实是性能和开发的一个大问题。

顺便说一句,我自己参与过一个源码超过5GB的网站。这个比一般的更难。

考虑一个由数千个DIV组成的DOM。记住,我们是现代Web开发者,我们的应用是单页应用!我又一大堆处理事件方法——点击,提交,输入等,一个典型的类jQuery事件处理就像下面所描述的那样:

  • 寻找所有和某事件相关的节点
  • 如果需要的话,对其更新

该做法有如下两个问题:

  1. 真的很难管理。假设你需要对一个事件处理方法进行微调。如果你丢失了上下文,你不得不钻源码钻得很深以知道到底发生了些什么事。很费时间,且很容易出问题。
  2. 很没效率。我们真的需要这些手动查询(节点)的结果吗?也许我们可以做的更聪明一点,辨别出哪些节点是需要被更新的。

强调一下,React是来解决问题的。第一个问题的答案是表述式(declarativeness)。相比类似手动遍历DOM树这种低层技术,你只需要声明一个组件应该怎么展现。React来为你做低层工作——实现工作表层下的HTML DOM API方法。React不需要你担心这个问题——最终,组件就像你声明的那样。

但是这并没有解决性能问题。而这也正是虚拟DOM发威的地方。

虚拟DOM

首先 - 虚拟DOM不是React发明的,但是React用了它且免费提供。

虚拟DOM是HTML DOM的抽象。它是轻量的,是从浏览器特定(Browser-specific,这里意指特定的浏览器需要特定的实现)实现细节中提取出来的。由于DOM本身就已经是一个抽象了,所以虚拟DOM,实际上,是一个抽象的抽象。

也许把虚拟DOM当做React的本地和简化版的HTML DOM更好。它允许React跳过既慢又限于特定浏览器的真实DOM操作,以在这个抽象世界中做自己的计算。

常规DOM和虚拟DOM二者并没有什么大的不同。这也是为什么React代码的JSX部分可以看起来几乎跟纯HTML很像的原因。

1
2
3
4
5
6
7
8
9
10
11
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<div>
Hello, world! I am a CommentBox.
</div>
</div>
);
}
});

在大多数情况下,当你有一段HTML代码且想要将其写成一个React组件时,你只需要做这个。

  1. 在render方法中返回HTML代码;
  2. 将class属性替换成className属性,因为class在JavaScript中是一个保留关键字。

二者之间还有一些,相当细微的区别。

  • 虚拟DOM的这些属性不在真实的DOM中出现——key,ref和dangerouslySetInnerHTML。查看更多
  • React范的DOM引入了一些限制

ReactElement vs ReactComponent

当讨论虚拟DOM时,明白React元素和React组件的区别是很重要的。

ReactElement

这是React中的主类型。在React官方文档中有:

一个ReactElement是一个轻量的,无状态的,不可变的,DOM元素的虚拟表示。

ReactElement存在于虚拟DOM中。在这里它们构成了基本的节点。其不可变性使其更易于和快速比较和更新。这也是React性能优越的原因之一。

那么ReactElement具体是什么呢?答案是几乎所有的的HTML标签——divtablestrong等,只要你想用,这里查看整个列表。

一旦定义,ReactElement可以被渲染到真实DOM中去。之后React就停止了对element的控制。他们可以使慢的,普通的DOM节点。

1
2
3
4
var root = React.createElement('div');
ReactDOM.render(root, document.getElementById('example'));
// If you are surprised by the fact that `render`
// comes from `ReactDOM` package, see the Post Scriptum.

JSX编译HTML标签到ReactElement中。所以上面的代码等同于。

1
2
var root = <div />;
ReactDOM.render(root, document.getElementById('example'));

强调一遍——ReactElement是React式虚拟DOM的基本元素。且他们是无状态的,因此看上去对程序员可能没什么用。我们可能宁愿写类class的HTML代码片段,带有诸如变量常量的代码片段——是吧?所以我们需要下面的东西。

ReactComponent

ReactComponentReactElement的区别在于,ReactComponent是有状态的。

我们一般使用React.createClass方法来定义一个ReactElement

1
2
3
4
5
6
7
8
9
10
11
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<div>
Hello, world! I am a CommentBox.
</div>
</div>
);
}
});

从渲染方法中返回的类HTML块可以有状态。最棒的是(我估计你已经知道了,这也是React厉害的原因),无论状态何时发生改变,组件都会重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var Timer = React.createClass({
getInitialState: function() {
return {secondsElapsed: 0};
},
tick: function() {
this.setState({secondsElapsed: this.state.secondsElapsed + 1});
},
componentDidMount: function() {
this.interval = setInterval(this.tick, 1000);
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
render: function() {
return (
<div>
<div>
Seconds Elapsed: {this.state.secondsElapsed}
</div>
</div>
);
}
});

ReactComponent是一个设计动态HTML的极优秀的工具。它没有访问虚拟DOM的权限,但可以轻易地转换为ReactElement

1
2
3
var element = React.createElement(MyComponent);
// or equivalently, with JSX
var element = <MyComponent />;

有意义的部分

ReactComponent很易于管理,应该被大量使用。但是他们也没有访问虚拟DOM的权限,而且我们需要对虚拟DOM做一些操作。

ReactComponent在改变状态时,我们希望尽可能少的改变真实DOM。所以React就是这么做的。ReactComponent会被转换成ReactElement。现在ReactElement可以被插入虚拟DOM中,易于快速比较和更新。具体是怎么做的呢?好吧,这部分是diff算法的工作。关键点是,这种做法比在比较和修改真实DOM更快

当React知道了diff的时候,它会在转换成低层的(HTML DOM)代码,这里作用的目标就是真实DOM了。这些代码会根据不同的浏览器进行相关的优化。

总结

虚拟DOM真的是主页中夸的那样吗?我觉得是的。在实践中React的性能是绝对好的,而虚拟DOM也是功不可没。

PS. 当React0.14发布的时候,React的DOM相关的部分从React包中抽取出来了。现在有一个独立的包叫react-dom。详情见最新的changelog

PS. In case you didn’t notice - since the last week, when React 0.14 was released, the DOM-related parts of React were extracted from the react package. Now there is a separate package react-dom. You can read more in the newest changelog.

参考

  1. The difference between Virtual DOM and DOM - Bartosz Krajka