总结:为了防止太长不看,简单总结一下Virtual Dom到底做了什么。DOM操作很慢是两个原因,一个是本身操作就不快,第二是我们(还有很多框架)处理dom的方式很慢,Virtual Dom解决了我们这些愚蠢的程序员对Dom的低劣操作,它让我们不需要进行Dom操作,而是将希望展现的最终结果告诉React,React通过一个简化的Dom即Virtual dom进行render,当你试图改变显示内容时,新生成的Virtual Dom会与现在的Virtual dom对比,通过diff算法找到区别,这些操作都是在快速的js中完成的,最后对实际Dom进行最小的Dom操作来完成效果,这就是Virtual Dom的概念。
Virtual Dom快的前提
1.1 Javascript很快
Julia有一个Benchmark,Julia Benchmarks, 可以看到Javascript跟C语言很接近了,也就几倍的差距,跟Java基本也是一个量级。
1.2 DOM很慢
当你用document.createElement()创建一个空的Element的时候(比如创建一个空的div),有以下这几页的东西需要实现(当然,这不是标准,只是个大概的意思):
HTMLElement - Web API Interfaces**
Element - Web API Interfaces**
非常非常多,并且还有不少嵌套引用。
你可以在Chrome console里手动调用document.createElement 然后插入DOM里看看效果。
这还是一个空的Elemnt,啥内容也没有,就这么复杂。所以说DOM的操作非常慢是可以理解的。不是浏览器不想好好实现DOM,而是DOM设计得太复杂,没办法。
而更糟糕的是,我们(以及很多框架)在调用DOM的API的时候做得不好,导致整个过程更加的慢。React的Virtual Dom解决的是这一部分问题,它并不能解决DOM本身慢的问题。
比如说,现在你的list是这样,
- 0
- 1
- 2
- 3
你想把它变成这样
- 6
- 7
- 8
- 9
- 10
通常的操作是什么?
先把0, 1,2,3这些Element删掉,然后加几个新的Element 6,7,8,9,10进去,这里面就有4次Element删除,5次Element添加。
而React会把这两个做一下Diff,然后发现其实不用删除0,1,2,3,而是可以直接改innerHTML,然后只需要添加一个Element(10)就行了,这样就是4次innerHTML操作加1个Element添加,比9次Element操作快多了吧?
关于React
2.1 接口和设计
在React的设计里,是完全不需要你操作DOM的。在React里其实根本就没有DOM这个概念的存在,只有Component。当你写好一个Component以后,Component会完全负责UI,你不需要也不应该去也不能够指挥Component怎么显示,你只能告诉它你想要显示一个香蕉还是两个梨。
2.2 实现
那么,如何实现React呢?其实对于React来说,最容易实现的办法是每次完全摧毁整个DOM,然后重新建立一个全新的DOM。因为一个Component是一个Pure function,根本就没有State这个概念,我又不知道DOM现在是什么样子,那最简单的办法当然是只要你给新数据,我就把整个DOM删了,然后根据你给的数据重新生成一个DOM咯。
等等,Virtual DOM哪儿去了?
事实是这样的,最简单实现React的方式虽然说非常简单,但是效率实在是太低了,你居然要全部都删了重建DOM,DOM本身已经很慢了,你还这么去用,谁能忍啊?
然后Virtual DOM就来救场了。
Virtual DOM和DOM是啥关系呢?
首先,Virtual DOM并没有完全实现DOM,Virtual DOM最主要的还是保留了Element之间的层次关系和一些基本属性。因为DOM实在是太复杂,一个空的Element都复杂得能让你崩溃,并且几乎所有内容我根本不关心好吗。所以Virtual DOM里每一个Element实际上只有几个属性,并且没有那么多乱七八糟的引用。所以哪怕是直接把Virtual DOM删了,根据新传进来的数据重新创建一个新的Virtual DOM出来都非常非常非常快。(每一个component的render函数就是在做这个事情,给新的virtual dom提供input)
所以,引入了Virtual DOM之后,React是这么干的:
你给我一个数据,我根据这个数据生成一个全新的Virtual DOM,然后跟我上一次生成的Virtual DOM去 diff,得到一个Patch,然后把这个Patch打到浏览器的DOM上去。完事。
有点像版本控制打patch的思路。
假设在任意时候有,VirtualDom1 == DOM1 (组织结构相同)
当有新数据来的时候,我生成VirtualDom2,然后去和VirtualDom1做diff,得到一个Patch。
然后将这个Patch去应用到DOM1上,得到DOM2。
如果一切正常,那么有VirtualDom2 == DOM2。
这里你可以做一些小实验,去破坏VirtualDom1 == DOM1这个假设(手动在DOM里删除一些Element,这时候VirtualDom里的Element没有被删除,所以两边不一样了)。
然后给新的数据,你会发现生成的界面就不是你想要的那个界面了。
最后,回到为什么Virtual Dom快这个问题上。
其实是由于每次生成virtual dom很快,diff生成patch也比较快,而在对DOM进行patch的时候,我能够根据Patch的内容,优化一部分DOM操作,比如之前1.2里的那个例子。
重点就在最后,哪怕是我生成了virtual dom,哪怕是我跑了diff,但是我根据patch简化了那些DOM操作省下来的时间依然很可观。所以总体上来说,还是比较快。
Virtual Dom到底做了什么
浏览器上呈现的html文档,本质上来说是一种xml,那么我们可以用一种树状结构把这个html文档描述出来:
1 | html |
React在呈现的过程中,会首先根据render的结果将这个树状结构在js里创建出来(注意,这个时候并没有操作DOM),这个树状结构就是虚拟DOM层。根据一个array[1, 2, 3, 4, 5]去渲染一个列表,那么虚拟DOM应该类似以下结构:
1 | ul |
这时再根据这个虚拟DOM渲染成实际DOM。然后重点来了,如果array内容发生变化了怎么办,比如我们删除了3这个元素,重新render,虚拟DOM会发生相应变化:
1 | ul |
React会将这个新的虚拟DOM和正在呈现的虚拟DOM进行对比,并找出其中的差异,然后用最少的DOM操作完成这个更新。
这里需要注意到,以上这些操作都是在js里完成的(生成虚拟DOM,比对),并没有实际操作DOM元素,比对完毕后找出的差异才会实际操作DOM元素,比如移除掉一个节点,更新其他节点的属性。
对比angularjs,没有比对的这个过程,直接移除掉所有模板元素的DOM,再重新添加。我们曾经结合angularjs和d3js,模板中有一个占位符用d3js来绘制图表,结果数据变化时,d3js绘制的图表被移除了,替换回了占位符,不得不重新绘制图表,而不是更新图表数据,结果就是图表会有一瞬间的空白状态。
具体的diff算法React的diff算法,防白板:http://blog.jobbole.com/73701/
以上内容整理自知乎用户EMayej Bee和周聪在怎么更好的理解虚拟DOM中的回答