React引入了虚拟DOM的概念,以提升性能!
React里,当state或者props改变的时候,render函数会重新执行,重新渲染页面。假设没有React,我们该怎么实现这个过程?
- state数据
- JSX模板
- 数据+模板 结合,生成真实的DOM,挂载到页面上显示。
- state发生改变。
- 数据+模板 结合,生成新的真实的DOM,替换原始的DOM,挂载到页面上展示。
这种方式的缺陷:(生成和替换DOM,会非常耗费性能)
步骤3,第一次生成了一个完整的DOM片段。
步骤5,第二次生成了一个完整的DOM片段。
步骤5,第二次的DOM,替换第一次的DOM。
对上边的方法进行改良
- state数据
- JSX模板
- 数据+模板 结合,生成真实的DOM,挂载到页面上显示。
- state发生改变。
- 数据+模板 结合,生成真实的DOM,并不直接替换原始的DOM
- 新的DOM
(这里新的DOM ,就是DocumentFrament 是JS底层的文档碎片,它是在内存里,并没有真实挂载倒页面)
和 原始的DOM做比对,找出差异。 - 找到哪些是修改的,有差异的DOM。
- 只用新的DOM中的变化的元素,替换掉老的DOM中的元素。
这种方法的缺陷,性能提升并不明细
相比上边直接生成真实DOM并全部替换原始DOM,节约了性能。但是又损耗了一部分性能,那就是新的DOM和原始DOM做比对消化的性能。
虚拟DOM方案
- state数据
- JSX模板
- 数据+模板 结合,生产虚拟DOM
(虚拟DOM就是一个JS对象,用它来描述真实DOM)
。
// 真实DOM节点
<ul id='list'>
<li class='item'>hello</li>
</ul>
// 虚拟DOM对象
var element = {
tagName: 'ul', // 节点标签名
props: { // DOM的属性,用一个对象存储键值对
id: 'list'
},
children: [ // 该节点的子节点
{tagName: 'li', props: {class: 'item'}, children: ["hello"]},
]
}
- 用虚拟DOM的结构,生成真实的DOM,挂载到页面上显示。
- state发生变化
- 数据+模板结合,生成新的虚拟DOM
(只生成新的虚拟DOM, 极大提升了性能)
// 新的虚拟DOM
var element = {
tagName: 'ul', // 节点标签名
props: { // DOM的属性,用一个对象存储键值对
id: 'list'
},
children: [ // 该节点的子节点
{tagName: 'li', props: {class: 'item'}, children: ["world"]},
]
}
- 比较原始虚拟DOM和新的虚拟DOM的区别,找到具体区别,直接操作DOM改变不一样的节点。(对比JS对象,极大提升了性能)
注意,只所以说极大的提升了性能:(用JS创建一个JS对象,并对比JS对象的差异,非常简单,性能代价很小,但是如果要创建一个真实DOM,需要调用Web Application级别的API,这个级别的API,性能损耗非常大)
注意JSX语法,并不是真实的DOM,而是一个模板, 没有JSX语法我们完全可以用React.createElement()来实现同样的功能,只不过比较麻烦。之所以用JSX语法,是因为这种语法,写起来更简洁。
// JSX语法 -> createElement -> 虚拟DOM(JS对象) -> 真实DOM
render(){
return (
<div>item</div>
)
}
render(){
return React.createElement('div',{},'item')
}
虚拟DOM,带来了什么好处?
- 性能提升了。
- 它使得跨端应用得以实现。React Native (用react语法去写原生应用)。为什么这么说呢,假如没有虚拟DOM,数据发生改变,拿到JSX模板,然后去渲染DOM,这在浏览器上是没有问题的,但是,在移动端的原生应用里,比如安卓或IOS机器上的代码里,是没有DOM的概念的,如果没有虚拟DOM,生成的DOM在原生应用里根本就无法使用。这个时候,我们的代码,只能运行在浏览器里。但是有了虚拟DOM ,因为它是一个JS对象,这个对象,在浏览器里和在原生应用里,都可以被识别。只不过,在浏览器里,虚拟dom用来生成真实DOM,在原生应用里,用来生成原生组件,所以,state和JSX模板,都可以被复用。所以,React即可以开发网页应用,又可以开发原生应用。
虚拟DOM中的Diff算法
- 新老虚拟DOM差异的比对,需要用到diff算法,diff全称是diffrence(差异)。这种算法大大提升了两个虚拟DOM的比对性能。
- 在React中,虚拟DOM,什么时候会被比对?当数据发生改变的时候,虚拟DOM才会被比对,数据的变化,要么是state,要么是props发生了改变,而props的改变,还是因为父组件的state发生了改变,所以归根节点,什么时候数据才会发生变化呢? 也就是调用
setState
方法的时候,数据才会发生变化,然后虚拟DOM才会发生比对。 -
setState
方法是异步的,之所以设计为异步的,本质是为了提升react底层的性能,如果我们连续调用了3次setState
方法,变更3组数据,此时页面会怎么做?会做3次虚拟DOM的比对,更新3次页面,那么如果这3次调用间隔时间都非常的小呢?做3次虚拟DOM的比对,更新3次页面会非常的浪费性能,此时react遇到连续调用3次setState
方法的情况会怎么做呢? react会把3次setState
,合并成1个setState
方法,然后做一次虚拟DOM的比对,去更新一次DOM,这样就省去了额外的两次DOM比对造成的性能上的耗费。 - diff算法采用
同层比对方法
。看下图,左侧是一开始生产的虚拟DOM,右侧是当数据发生变化的时候,生成的新的虚拟DOM,两个虚拟DOM做比对,找到差异后,去更新真实DOM。如果最顶层的DOM一致,再去比对下一层,以此类推,假设,虚拟DOM比对的时候,第一层DOM节点 就不一致,react就不会再继续往下比了,react会把原始页面的虚拟DOM这个节点下面的所有DOM,全部删除,重新生成一遍这个节点下的所有的DOM,用重新生成的DOM,替换原始页面上的DOM。哪怕第二层,第三层的DOM没有发生变化,也会删除重新生成,没法复用,这虽然会造成DOM节点渲染的浪费,但是这种同层比对的好处就是比对算法非常简单,只要一层一层往下的比对就行了,算法简单了,比对速度就非常快,大大减少了两个虚拟DOM比对的性能上的消耗
image.png - 虚拟DOM的key值。如果没有key值,每个虚拟DOM节点就没有自己的名字,当我们做两个虚拟DOM树的比对的时候,节点和节点之间的关系,就会很难被确立。比对的时候,就需要做多层循环的比较,比较耗费性能。假如,我们在做虚拟DOM节点的循环的时候,可以给每个节点起个名字
(也就是key)
,当数据发生变化的时候,虚拟DOM节点的比对会根据key值做关联,key一致的就可以复用了,只需要把key不一致的,增加到DOM树上就可以了,极大的提升了虚拟DOM比对的性能,前提是老的虚拟DOM节点上的key到了新的虚拟DOM上,还是保持原来的值,没有发生变化,这就是为什么我们在循环的时候,尽量不要用index做key
的原因。key是index的话,是不稳定的,没有办法保证老的虚拟DOM的key跟新的虚拟DOM的key一致。所以,我们要用一个稳定的值,作为key,尽量不用index
image.png
网友评论