抛砖引玉
React 通过引入 Virtual DOM 的概念,极大地防止有效的 Dom 操作,已使咱们的页面的构建效率提到了极大的晋升。然而如何高效地通过比照新旧 Virtual DOM 来找出真正的 Dom 变动之处同样也决定着页面的性能,React 用其非凡的 diff 算法解决这个问题。Virtual DOM+React diff 的组合极大地保障了 React 的性能,使其在业界有着不错的性能口碑。diff 算法并非 React 独创,React 只是对 diff 算法做了一个优化,但却是因为这个优化,给 React 带来了极大的性能晋升,不禁让人感叹 React 创造者们的智慧!接下来咱们就探索一下 React 的 diff 算法。
传统 diff 算法
在文章结尾咱们提到 React 的 diff 算法给 React 带来了极大的性能晋升,而之前的 React diff 算法是在传统 diff 算法上的优化。上面咱们先看一下传统的 diff 算法是什么样子的。
传统 diff 算法通过循环递归对节点进行顺次比照,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。
O(n^3) 到底有多可怕呢?这意味着如果要展现 1000 个节点,就要顺次执行上十亿次 的比拟,这种指数型的性能耗费对于前端渲染场景来说代价太高了。而 React 却这个 diff 算法工夫复杂度从 O(n^3)降到 O(n)。O(n^3)到 O(n)的晋升有多大,咱们通过一张图来看一下。
从下面这张图来看,React 的 diff 算法所带来的晋升无疑是微小无比的。接下来咱们再看一张图:
从 1979 到 2011,30 多年的工夫,才将工夫复杂度搞到 O(n^3),而 React 从开源到当初不过区区几年的工夫,却一下子干到 O(n),到这里不禁再次膜拜一下 React 的创造者们。
那么 React 这个牛逼的 diff 算法是如何做到的呢?
React diff 原理
后面咱们讲到传统 diff 算法的工夫复杂度为 O(n^3), 其中 n 为树中节点的总数,随着 n 的减少,diff 所消耗的工夫将出现爆炸性的增长。react 却利用其非凡的 diff 算法做到了 O(n^3)到 O(n)的飞跃性的晋升,而实现这一壮举的法宝就是上面这三条看似简略的 diff 策略:
- Web UI 中 DOM 节点跨层级的挪动操作特地少,能够忽略不计。
- 领有雷同类的两个组件将会生成类似的树形构造,领有不同类的两个组件将会生成不同的树形构造。
- 对于同一层级的一组子节点,它们能够通过惟一 id 进行辨别。
在下面三个策略的根底上,React 别离将对应的 tree diff、component diff 以及 element diff 进行算法优化,极大地晋升了 diff 效率。
tree diff
基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比拟,两棵树只会对同一档次的节点进行比拟。
既然 DOM 节点跨层级的挪动操作少到能够忽略不计,针对这一景象,React 只会对雷同层级的 DOM 节点进行比拟,即同一个父节点下的所有子节点。当发现节点曾经不存在时,则该节点及其子节点会被齐全删除掉,不会用于进一步的比拟。这样只须要对树进行一次遍历,便能实现整个 DOM 树的比拟。参考 React 实战视频解说:进入学习
策略一的前提是 Web UI 中 DOM 节点跨层级的挪动操作特地少,但并没有否定 DOM 节点跨层级的操作的存在,那么当遇到这种操作时,React 是如何解决的呢?
接下来咱们通过一张图来展现整个处理过程:
A 节点 (包含其子节点) 整个被挪动到 D 节点下,因为 React 只会简略地思考同层级节点的地位变换,而对于不 同层级的节点,只有创立和删除操作。当根节点发现子节点中 A 隐没了,就会间接销毁 A; 当 D 发现多了一个子节点 A,则会创 建新的 A(包含子节点)作为其子节点。此时,diff 的执行状况:create A → create B → create C → delete A。
由此能够发现,当呈现节点跨层级挪动时,并不会呈现设想中的挪动操作,而是以 A 为根节点的整个树被从新创立。这是一种影响 React 性能的操作,因而官网倡议不要进行 DOM 节点跨层级的操作。
在开发组件时,保持稳定的 DOM 构造会有助于性能的晋升。例如,能够通过 CSS 暗藏或显示节点,而不是真正地移
除或增加 DOM 节点。
component diff
React 是基于组件构建利用的,对于组件间的比拟所采取的策略也是十分简洁、高效的。
- 如果是同一类型的组件,依照原策略持续比拟 Virtual DOM 树即可。
- 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
- 对于同一类型的组件,有可能其 Virtual DOM 没有任何变动,如果可能确切晓得这点,那么就能够节俭大量的 diff 运算工夫。因而,React 容许用户通过 shouldComponentUpdate()来判断该组件是否需进行 diff 算法剖析,然而如果调用了 forceUpdate 办法,shouldComponentUpdate 则生效。
接下来咱们看上面这个例子是如何实现转换的:
转换流程如下:
当组件 D 变为组件 G 时,即便这两个组件构造类似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比拟二 者的构造,而是间接删除组件 D,从新创立组件 G 及其子节点。尽管当两个组件是不同类型但构造类似时,diff 会影响性能,但正如 React 官网博客所言: 不同类型的组件很少存在类似 DOM 树的状况,因而这种极其因素很难在理论开发过程中造成重大的影响。
element diff
当节点处于同一层级时,diff 提供了 3 种节点操作,别离为 INSERT_MARKUP (插入)、MOVE_EXISTING (挪动)和 REMOVE_NODE (删除)。
- INSERT_MARKUP : 新的组件类型不在旧汇合里,即全新的节点,须要对新节点执行插入操作。
- **MOVE_EXISTING : 旧汇合中有新组件类型,且 element 是可更新的类型,generateComponentChildren 已调用
receiveComponent,这种状况下 prevChild=nextChild,就须要做挪动操作,能够复用以前的 DOM 节点。** - **REMOVE_NODE : 旧组件类型,在新汇合里也有,但对应的 element 不同则不能间接复用和更新,须要执行删除操作,或者
旧组件不在新汇合里的,也须要执行删除操作。**
旧汇合中蕴含节点 A、B、C 和 D,更新后的新汇合中蕴含节点 B、A、D 和 C,此时新旧汇合进行 diff 差异化比照,发现 B!=A,则创立并插入 B 至新汇合,删除旧汇合 A; 以此类推,创立并插入 A、D 和 C,删除 B、C 和 D。
咱们发现这些都是雷同的节点,仅仅是地位产生了变动,但却须要进行繁冗低效的删除、创立操作,其实只有对这些节点进行地位挪动即可。React 针对这一景象提出了一种优化策略:容许开发者对同一层级的同组子节点,增加惟一 key 进行辨别。 尽管只是小小的改变,性能上却产生了天翻地覆的变动! 咱们再来看一下利用了这个策略之后,react diff 是如何操作的。
通过 key 能够精确地发现新旧汇合中的节点都是雷同的节点,因而无需进行节点删除和创立,只须要将旧汇合中节点的地位进行挪动,更新为新汇合中节点的地位,此时 React 给出的 diff 后果为:B、D 不做任何操作,A、C 进行挪动操作即可。
具体的流程咱们用一张表格来展示一下:
index | 节点 | oldIndex | maxIndex | 操作 |
---|---|---|---|---|
0 | B | 1 | 0 | oldIndex(1)>maxIndex(0),maxIndex=oldIndex,maxIndex 变为 1 |
1 | A | 0 | 1 | oldIndex(0)<maxIndex(1), 节点 A 挪动至 index(1)的地位 |
2 | D | 3 | 1 | oldIndex(3)>maxIndex(1),maxIndex=oldIndex,maxIndex 变为 3 |
3 | C | 2 | 3 | oldIndex(2)<maxIndex(3), 节点 C 挪动至 index(3)的地位 |
- index:新汇合的遍历下标。
- oldIndex:以后节点在老汇合中的下标。
- maxIndex:在新汇合拜访过的节点中,其在老汇合的最大下标值。
操作一栏中只比拟 oldIndex 和 maxIndex:
- 当 oldIndex>maxIndex 时,将 oldIndex 的值赋值给 maxIndex
- 当 oldIndex=maxIndex 时,不操作
- 当 oldIndex<maxIndex 时,将以后节点挪动到 index 的地位
下面的例子仅仅是在新旧汇合中的节点都是雷同的节点的状况下,那如果新汇合中有新退出的节点且旧汇合存在 须要删除的节点,那么 diff 又是如何比照运作的呢?
index | 节点 | oldIndex | maxIndex | 操作 |
---|---|---|---|---|
0 | B | 1 | 0 | oldIndex(1)>maxIndex(0),maxIndex=oldIndex,maxIndex 变为 1 |
1 | E | – | 1 | oldIndex 不存在,增加节点 E 至 index(1)的地位 |
2 | C | 2 | 1 | oldIndex(2)>maxIndex(1),maxIndex=oldIndex,maxIndex 变为 2 |
3 | A | 0 | 2 | oldIndex(0)<maxIndex(2), 节点 A 挪动至 index(3)的地位 |
注:最初还须要对旧汇合进行循环遍历,找出新汇合中没有的节点,此时发现存在这样的节点 D,因而删除节点 D,到此 diff 操作全副实现。
同样操作一栏中只比拟 oldIndex 和 maxIndex,然而 oldIndex 可能有不存在的状况:
-
oldIndex 存在
- 当 oldIndex>maxIndex 时,将 oldIndex 的值赋值给 maxIndex
- 当 oldIndex=maxIndex 时,不操作
- 当 oldIndex<maxIndex 时,将以后节点挪动到 index 的地位
-
oldIndex 不存在
-
新增以后节点至 index 的地位
-
当然这种 diff 并非白璧无瑕的,咱们来看这么一种状况:
理论咱们只需对 D 执行挪动操作,然而因为 D 在旧汇合中的地位是最大的,导致其余节点的 oldIndex < maxIndex,造成 D 没有执行挪动操作,而是 A、B、C 全副挪动到 D 节点前面的景象。针对这种状况,官网倡议:
在开发过程中,尽量减少相似将最初一个节点挪动到列表首部的操作。当节点数量过大或更新操作过于频繁时,这在肯定水平上会影响 React 的渲染性能。
因为 key 的存在,react 能够精确地判断出该节点在新汇合中是否存在,这极大地提高了 diff 效率。咱们在开发过中进行列表渲染的时候,若没有加 key,react 会抛出正告要求开发者加上 key,就是为了进步 diff 效率。然而加了 key 肯定要比没加 key 的性能更高吗?咱们再来看一个例子:
当初有一汇合[1,2,3,4,5], 渲染成如下的样子:<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
---------------
当初咱们将这个汇合的程序打乱变成[1,3,2,5,4]。1. 加 key
<div key='1'>1</div> <div key='1'>1</div>
<div key='2'>2</div> <div key='3'>3</div>
<div key='3'>3</div> ========> <div key='2'>2</div>
<div key='4'>4</div> <div key='5'>5</div>
<div key='5'>5</div> <div key='4'>4</div>
操作:节点 2 挪动至下标为 2 的地位,节点 4 挪动至下标为 4 的地位。2. 不加 key
<div>1</div> <div>1</div>
<div>2</div> <div>3</div>
<div>3</div> ========> <div>2</div>
<div>4</div> <div>5</div>
<div>5</div> <div>4</div>
操作:批改第 1 个到第 5 个节点的 innerText
---------------
如果咱们对这个汇合进行增删的操作改成[1,3,2,5,6]。1. 加 key
<div key='1'>1</div> <div key='1'>1</div>
<div key='2'>2</div> <div key='3'>3</div>
<div key='3'>3</div> ========> <div key='2'>2</div>
<div key='4'>4</div> <div key='5'>5</div>
<div key='5'>5</div> <div key='6'>6</div>
操作:节点 2 挪动至下标为 2 的地位,新增节点 6 至下标为 4 的地位,删除节点 4。2. 不加 key
<div>1</div> <div>1</div>
<div>2</div> <div>3</div>
<div>3</div> ========> <div>2</div>
<div>4</div> <div>5</div>
<div>5</div> <div>6</div>
操作:批改第 1 个到第 5 个节点的 innerText
---------------
通过下面这两个例子咱们发现:因为 dom 节点的挪动操作开销是比拟低廉的,没有 key 的状况下要比有 key 的性能更好。
通过下面的例子咱们发现,尽管加了 key 进步了 diff 效率,然而未必肯定晋升了页面的性能。因而咱们要留神这么一点:
对于简略列表页渲染来说,不加 key 要比加了 key 的性能更好
依据下面的状况,最初咱们总结一下 key 的作用:
- 精确判断出以后节点是否在旧汇合中
- 极大地缩小遍历次数
利用实际
页面指定区域刷新
当初有这么一个需要,当用户身份变动时,以后页面从新加载数据。猛一看过来感觉非常简单,没啥难度的,只有在 componentDidUpdate 这个生命周期里去判断用户身份是否产生扭转,如果产生扭转就从新申请数据,于是就有了以下这一段代码:
import React from 'react';
import {connect} from 'react-redux';
let oldAuthType = '';// 用来存储旧的用户身份
@connect(state=>state.user)
class Page1 extends React.PureComponent{
state={loading:true}
loadMainData(){
// 这里采纳了定时器去模仿数据申请
this.setState({loading:true});
const timer = setTimeout(()=>{
this.setState({loading:false});
clearTimeout(timer);
},2000);
}
componentDidUpdate(){const {authType} = this.props;
// 判断以后用户身份是否产生了扭转
if(authType!==oldAuthType){
// 存储新的用户身份
oldAuthType=authType;
// 从新加载数据
this.loadMainData();}
}
componentDidMount(){
oldAuthType=this.props.authType;
this.loadMainData();}
render(){const {loading} = this.state;
return (<h2>{` 页面 1${loading?'加载中...':'加载实现'}`}</h2>
)
}
}
export default Page1;
看上去咱们仅仅通过加上一段代码就实现了这一需要,然而当咱们页面是几十个的时候,那这种办法就显得顾此失彼了。哪有没有一个很好的办法来实现这个需要呢?其实很简略,利用 react diff 的个性就能够实现它。对于这个需要,实际上就是心愿以后组件能够销毁在从新生成,那怎么能力让其销毁并从新生成呢?通过下面的总结我发现两种状况,能够实现组件的销毁并从新生成。
- 当组件类型产生扭转
- 当 key 值发生变化
接下来咱们就联合这两个特点,用两种办法去实现。
第一种:引入一个 loading 组件。切换身份时设置 loading 为 true,此时 loading 组件显示;切换身份实现,loading 变为 false,其子节点 children 显示。
<div className="g-main">{loading?<Loading/>:children}</div>
第二种:在刷新区域加上一个 key 值就能够了,用户身份一扭转,key 值就产生扭转。
<div className="g-main" key={authType}>{children}</div>
第一种和第二种取舍上,集体倡议的是这样子的:
如果须要申请服务器的,用第一种,因为申请服务器会有肯定等待时间,退出 loading 组件能够让用户有感知,体验更好。如果是不须要申请服务器的状况下,选用第二种,因为第二种更简略实用。
更加不便地监听 props 扭转
针对这个需要,咱们喜爱将搜寻条件封装成一个组件,查问列表封装成一个组件。其中查问列表会接管一个查问参数的属性,如下所示:
import React from 'react';
import {Card} from 'antd';
import Filter from './components/filter';
import Teacher from './components/teacher';
export default class Demo2 extends React.PureComponent{
state={
filters:{
name:undefined,
height:undefined,
age:undefined
}
}
handleFilterChange=(filters)=>{
this.setState({filters});
}
render(){const {filters} = this.state;
return <Card>
{/* 过滤器 */} <Filter onChange={this.handleFilterChange}/> {/* 查问列表 */} <Teacher filters={filters}/>
</Card>
}
}
当初咱们面临一个问题,如何在组件 Teacher 中监听 filters 的变动,因为 filters 是一个援用类型,想监听其变动变得有些简单,好在 lodash 提供了比拟两个对象的工具办法,使其简略了。然而如果前期给 Teacher 加了额定的 props,此时你要监听多个 props 的变动时,你的代码将变得比拟难以保护。针对这个问题,咱们仍旧能够通过 key 值去实现,当每次搜寻时,从新生成一个 key,那么 Teacher 组件就会从新加载了。代码如下:
import React from 'react';
import {Card} from 'antd';
import Filter from './components/filter';
import Teacher from './components/teacher';
export default class Demo2 extends React.PureComponent{
state={
filters:{
name:undefined,
height:undefined,
age:undefined
},
tableKey:this.createTableKey()}
createTableKey(){return Math.random().toString(36).substring(7);
}
handleFilterChange=(filters)=>{
this.setState({
filters,
// 从新生成 tableKey
tableKey:this.createTableKey()});
}
render(){const {filters,tableKey} = this.state;
return <Card>
{/* 过滤器 */} <Filter onChange={this.handleFilterChange}/> {/* 查问列表 */} <Teacher key={tableKey} filters={filters}/>
</Card>
}
}
即便前期给 Teacher 退出新的 props,也没有问题,只需拼接一下 key 即可:
<Teacher key={`${tableKey}-${prop1}-${prop2}`} filters={filters} prop1={prop1} prop2={prop2}/>
react-router 中 Link 问题
先看一下 demo 代码:
import React from 'react';
import {Card,Spin,Divider,Row,Col} from 'antd';
import {Link} from 'react-router-dom';
const bookList = [{
bookId:'1',
bookName:'三国演义',
author:'罗贯中'
},{
bookId:'2',
bookName:'水浒传',
author:'施耐庵'
}]
export default class Demo3 extends React.PureComponent{
state={bookList:[],
bookId:'',
loading:true
}
loadBookList(bookId){
this.setState({loading:true});
const timer = setTimeout(()=>{
this.setState({
loading:false,
bookId,
bookList
});
clearTimeout(timer);
},2000);
}
componentDidMount(){const {match} = this.props;
const {params} = match;
const {bookId} = params;
this.loadBookList(bookId);
}
render(){const {bookList,bookId,loading} = this.state;
const selectedBook = bookList.find((book)=>book.bookId===bookId);
return <Card>
<Spin spinning={loading}>
{ selectedBook&&(<div>
<img width="120" src={`/static/images/book_cover_${bookId}.jpeg`}/>
<h4> 书名:{selectedBook?selectedBook.bookName:'--'}</h4>
<div> 作者:{selectedBook?selectedBook.author:'--'}</div>
</div>) } <Divider orientation="left"> 关联图书 </Divider>
<Row>
{bookList.filter((book)=>book.bookId!==bookId).map((book)=>{const {bookId,bookName} = book; return <Col span={6}>
<img width="120" src={`/static/images/book_cover_${bookId}.jpeg`}/>
<h4><Link to={`/demo3/${bookId}`}>{bookName}</Link></h4>
</Col>
}) } </Row>
</Spin>
</Card>
}
}
通过演示 gif,咱们看到了地址栏的地址曾经产生扭转,然而并没有咱们设想中那样从新走一遍 componentDidMount 去申请数据,这阐明咱们的组件并没有实现销毁并从新生成这么一个过程。解决这个问题你能够在 componentDidUpdate 去监听其扭转:
componentDidUpdate(){const {match} = this.props;
const {params} = match;
const {bookId} = params;
if(bookId!==this.state.bookId){this.loadBookList(bookId);
}
}
后面咱们说过如果是前期须要监听多个 props 的话,这样子前期保护比拟麻烦. 同样咱们还是利用 key 去解决这个问题,首页咱们能够将页面封装成一个组件 BookDetail,并且在其外层再包裹一层,再去给 BookDetail 加 key, 代码如下:
import React from 'react';
import BookDetail from './bookDetail';
export default class Demo3 extends React.PureComponent{render(){const {match} = this.props;
const {params} = match;
const {bookId} = params;
return <BookDetail key={bookId} bookId={bookId}/>
}
}
这样的益处是咱们代码构造更加清晰,后续拓展新性能比较简单。
结语:
- React 的高效得益于其 Virtual DOM+React diff 的体系。diff 算法并非 react 独创,react 只是在传统 diff 算法做了优化。但因为其优化,将 diff 算法的工夫复杂度一下子从 O(n^3)降到 O(n)。
-
React diff 的三大策略:
- Web UI 中 DOM 节点跨层级的挪动操作特地少,能够忽略不计。
- 领有雷同类的两个组件将会生成类似的树形构造,领有不同类的两个组件将会生成不同的树形构造。
- 对于同一层级的一组子节点,它们能够通过惟一 id 进行辨别。
- 在开发组件时,保持稳定的 DOM 构造会有助于性能的晋升。
- 在开发过程中,尽量减少相似将最初一个节点挪动到列表首部的操作。
- key 的存在是为了晋升 diff 效率,但未必肯定就能够晋升性能,记住简略列表渲染状况下,不加 key 要比加 key 的性能更好。
- 懂得借助 react diff 的个性去解决咱们理论开发中的一系列问题。