开篇咱们先举个简单而且常见的例子:javascript
<ul id='list'>
<li class='item'>1</li>
<li class='item'>2</li>
<li class='item'>3</li>
</ul>
复制代码
页面上有个list,数据依次是一、二、3,如今须要替换成四、五、六、7,若是不使用React,应该怎么操做?
方法1:removeChild()
清空列表,appendChild()
添加4个元素
方法2:针对前3个元素作nodeValue
/textContent
修改,而后appendChild()
添加1个元素
方法3:使用innerHtml
直接对整个列表作覆盖式操做html
以上3种方式都是使用原生DOM API,均可以实现效果,可是性能上会存在差别。形成差别的缘由多种多样,可能取决于元素类型,列表长度,甚至浏览器版本(万恶的IE)。所以应当根据当前环境灵活选用不一样的DOM操做方式,但这无疑增长了开发难度,不利于工程师专一实现当前业务。java
使用React的话就简单多了,咱们无需关心如何进行DOM操做,只须要把数据存在state
中,而后在render
函数中map
生成JSX代码。至于如何操做DOM,由React决定,这些都得益于虚拟DOM和diff算法。node
虚拟DOM就是使用javascript对象来表示真实DOM,是一个树形结构。
在真实DOM中,一个普通的div打印出来也是很复杂的: git
const tree = {
tagName: 'ul', // 节点标签名
props: { // DOM的属性,用一个对象存储键值对
id: 'list'
},
children: [ // 该节点的子节点
{tagName: 'li', props: {class: 'item'}, children: ['1']},
{tagName: 'li', props: {class: 'item'}, children: ['2']},
{tagName: 'li', props: {class: 'item'}, children: ['3']},
]
}
复制代码
虚拟DOM只保留了真实DOM节点的一些基本属性,和节点之间的层次关系,它至关于创建在javascript和DOM之间的一层“缓存”,能够类比CPU和硬盘:硬盘的读写速度是很慢的,相比较于廉价的内存来讲。github
React须要同时维护两棵虚拟DOM树:一棵表示当前的DOM结构,另外一棵在React状态变动将要从新渲染时生成。React经过比较这两棵树的差别,决定是否须要修改DOM结构,以及如何修改。这种算法称做Diff算法。算法
这个算法问题有一些通用的解决方案,即生成将一棵树转换成另外一棵树的最小操做数。 然而,即便在最前沿的算法中,该算法的复杂程度为
O(n 3 )
,其中n
是树中元素的数量。
若是在 React 中使用了该算法,那么展现 1000 个元素所须要执行的计算量将在十亿的量级范围。这个开销实在是太太高昂。因而 React 在如下两个假设的基础之上提出了一套O(n)
的启发式算法:数组
- 两个不一样类型的元素会产生出不一样的树;
- 开发者能够经过
key prop
来暗示哪些子元素在不一样的渲染下能保持稳定;
有人说虚拟DOM快,这也是相比较来讲的。因为维护虚拟DOM树和Diff算法的计算,简化了DOM操做,和MVVM变动整个DOM树的模式相比,确实节省了很多时间。可是和原生JS的开发模式相比,仍是直接操做DOM比较快一些,由于开发者明确地知道应该变动哪部分的DOM结构(前提是开发者了解最基本的DOM优化方法)。浏览器
Diff算法会对新旧两棵树作深度优先遍历,避免对两棵树作彻底比较,所以算法复杂度能够达到O(n)
。而后给每一个节点生成一个惟一的标志:缓存
在遍历的过程当中,每遍历到一个节点,就将新旧两棵树做比较,而且只对同一级别的元素进行比较:
也就是只比较图中用虚线链接起来的部分,把先后差别记录下来。
可能存在的差别类型以下:
举个例子,当一个元素从<a>
变成<img>
,从<Article>
变成<Comment>
,或从 <Button>
变成<div>
,都会触发一个完整的重建流程:该节点以及该节点的子节点,都会被销毁,以后建立新的节点。即 removeChild()
-> appendChild()
或者 setInnerHTML()
。
<div className="before" title="stuff" />
<div className="after" title="stuff" />
复制代码
当比对两个相同类型的元素时,React 会保留当前 DOM 节点和子节点,仅比对及更新有改变的属性。 经过比对这两个元素,React 知道只须要修改 DOM 元素上的 className
属性。即 removeAttribute()
-> setAttribute()
或者 setAttribute()
。
另外,React会针对style的改变作特殊处理:
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
复制代码
经过比对这两个元素,React 知道只须要修改 DOM 元素上的 color
样式,无需修改 fontWeight
。
若是是React组件,当一个组件更新时,组件实例保持不变,而且更新组件状态,调用该实例的 componentWillReceiveProps()
和 componentWillUpdate()
方法。下一步,调用 render()
方法,diff 算法将在以前的结果以及新的结果中进行递归。
<div>text 1</div>
<div>text 2</div>
复制代码
文本节点改动时,React会修改 nodeValue
/textContent
,这点无需赘述。
在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差别时,生成一个 mutation。
在子元素列表末尾新增元素时,更变开销比较小。好比:
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
复制代码
经过比较,前两个元素不存在差别,只须要在末尾插入一个<li>third</li>
。
若是简单实现的话,在列表头部插入会很影响性能,好比:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
复制代码
根据以前提到的算法规则,<li>Duke</li>
和<li>Villanova</li>
是不能被保留的,都会被看作差别部分,这样的变动开销会比较大。
为了解决以上问题,React 支持 key
属性。当子元素拥有 key
时,React 使用 key
来匹配原有树上的子元素以及最新树上的子元素。如下例子在新增 key
以后使得以前的低效转换变得高效:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
复制代码
Diff算法经过比较得知,key
为'2014'的元素是新增的,key
为'2015'和'2016'的元素仅仅是移动了位置,因此能够调用insertBefore()
来插入节点。
生成key的注意点:
key
在列表中应当具备惟一性,但不须要全局惟一。key
应当具备稳定性,一个节点在肯定key
以后就不该当变动key
(除非你但愿它从新渲染)。不稳定的 key
(好比在 render()
中经过 Math.random()
生成的 )会致使许多组件实例和 DOM 节点被没必要要地从新建立,这可能致使性能降低和子组件中的状态丢失。key
。这个策略在元素不进行从新排序时比较合适,但一旦有顺序修改,diff 就会变得慢。当基于下标的组件进行从新排序时,组件
state
可能会遇到一些问题。因为组件实例是基于它们的key
来决定是否更新以及复用,若是key
是一个下标,那么修改顺序时会修改当前的key
,致使非受控组件的state
(好比输入框)可能相互篡改致使没法预期的变更。
key
的使用方式是关乎于性能的,应尽可能避免以数组下标做为key
,而是以id的形式,或者数组下标 + id/name
。参考文章: