1. 单个react组件性能优化
1.1 render
里面尽量减少新建变量和bind
函数的使用,尽量减少传递参数的数量
在render
中绑定函数,无非就是下面三种:
render() {
return (
<div className='app'>
<span onClick={this.handleClick}>1</span>
<span onClick={this.handleClick.bind(this)}>2</span>
<span onClick={()=>this.handleClick()}>3</span>
</div>
)
}
第一种是在构造函数中绑定this
,第二种是在render()
函数里面绑定this
,第三种就是使用箭头函数,上述方法都能实现this
的绑定。
但是哪一种方法的性能最好,是我们要考虑的问题。毫无疑问第一种的性能最好。
第一种方法,构造函数每渲染一次便会执行一遍;
第二种方法,在每次render()
的时候都会重新执行一遍函数;
第三种方法,每一次render()
的时候,都会生成一个新的箭头函数,即使两个箭头函数的内容是一样的。
react
判断是否需要进行render
是浅层比较,简单来说就是通过===
来判断的,如果state
或者prop
的类型是字符串或者数字,只要值相同,那么浅层比较就会认为其相同;
但是如果前者的类型是复杂的对象的时候,我们知道对象是引用类型,浅层比较只会认为这两个prop
是不是同一个引用,如果不是,哪怕这两个对象中的内容完全一样,也会被认为是两个不同的prop
。
举个例子:
当我们给组件App
名为style
的prop
赋值;
<App style={{ color:"green" }}
使用这种方法,每一次渲染都会被认为是一个style
这个prop
发生了变化,因为每一次都会产生一个对象给style
。
如果想要让react
渲染的时候认为前后对象类型prop
相同,则必须要保证prop
指向同一个javascript
对象,改进如下:
const appStyle = { color: "red" }; //这个初始化只执行一次,不要放在render中,可以放在构造函数中
<App style={appStyle} />
1.2 定制shouldComponentUpdate
函数
生命周期函数shouldComponentUpdate
是决定react
组件什么时候能够重新渲染的函数,但是这个函数默认的实现方式就是简单的返回一个true
。也就是说,默认每次更新的时候都要调用所用的生命周期函数,包括render
函数,重新渲染。
看看下面这个例子:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count = 2,
name = 'apple',
}
this.handleClick = this.handleClick.bind(this);
this.handleName = this.handleName.bind(this);
}
this.handleClick() {
// ...
}
this.handleName() {
// ...
}
render() {
return (
<div className='app'>
<span>数量,{this.state.count}</span>
<button onClick={this.handleClick}>改变数量</button>
<button onClick={this.handleName}>改变名字</button>
<Child title={this.state.name}></Child>
</div>
);
}
}
class Child extends React.Component {
render() {
console.log('render了一次');
return (
<h3>我想吃,{this.props.title}</h3>
);
}
}
我们写了两个组件,App
和Child
组件,并写两个方法,一个改变App
中的count
的值,一个是改变name
,我们在Child
的render
中打印了每次是否执行。
不出意外,虽然Child
组件里的title
值没有改变,但是还是render
了。
为了进一步优化这个问题,我们这样改Child
组件:
class Child extends React.Component {
shouldComponentUpdate(nextProps,nextState) {
if(nextProps.title == this.props.title) {
return false;
}
return true;
}
render() {
console.log('render了一次');
return (
<h3>我想吃,{this.props.title}</h3>
);
}
}
只有当Child
的title
值发生改变的时候,组件才会去render
。
在最新的react
中,react
给我们提供了React.PureComponent
,官方也在早期提供了名为react-addons-pure-render-mixin
插件来重新实现shouldComponentUpdate
生命周期方法。
class Child extends React.PureComponent {
// shouldComponentUpdate(nextProps,nextState) {
// if(nextProps.title == this.props.title) {
// return false;
// }
// return true;
// }
render() {
console.log('render了一次');
return (
<h3>我想吃,{this.props.title}</h3>
);
}
}
通过上述的方法的效果也是和我们先前定制shouldComponentUpdate
的效果是一致的。
但是我们要注意的是,这里的PureRender
是浅比较的,因为深比较的场景是相当昂贵的。所以我们要注意我们在1.1
中说到的一些注意点:不要直接为props
设置对象或者数组、不要将方法直接绑定在元素上,因为其实函数也是对象。
1.3 Immutable.js
javascript
中的对象一般都是可变的,因为使用了引用赋值,新的对象简单的引用了原始对象,改变新对象将影响到原始对象。
举个例子:
student = { age : 1 };
school = student;
school.age = 2;
当我们给school.age
赋值后,会发现student.a
也变成了2,虽然我们可以通过深拷贝与浅拷贝解决这个问题,但是这样做非常的昂贵,对cpu
和内存会造成浪费。
这里就需要用到Immutable
,通过Immutable
创建的Immutable Data
一旦被创建,就不能再更改。对Immutable
对象进行修改、添加或删除操作,都会返回一个新的Immutable
对象。
下面是三个比较重要且用到的数据结构
Map
:键值对集合,对应Object,ES6中也有专门的Map对象List
:有序可重复列表,对应于ArrayArraySet
:有序且不可重复的列表
我们可以看一个例子:
使用Map
生成一个immutable
对象:
import { Map, is } from 'immutable';
let a = Map({
'name': 'apple',
'list': Map({name: 'orange'})
})
let b = a.set('name','banana');
console.log(a.get('course') === b.get('course')); // 返回true
console.log(a === b); // 返回false
Immutable.is
比较的是两个对象的 hashCode
或 valueOf
(对于JavaScript
对象)。由于immutable
内部使用了Trie
数据结构来存储,只要两个对象的 hashCode
相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。
Immutable
优点:
- 减少内存的使用
- 并发安全
- 降低项目的复杂度
- 便于比较复杂数据,定制
shouldComponentUpdate
方便 - 时间旅行功能
- 函数式编程
Immutable
缺点:
- 学习成本
- 库的大小(建议使用
seamless-immutable
) - 对现有项目入侵严重
- 容易与原生的对象进行混淆
2. 多个react组件性能优化
react
组件在装载过程中,react
通过在render
方法在内存中产生一个树形结构,树上的节点代表一个react
组件或者原生的Dom
元素,这个树形结构就是我们所谓的Vitural Dom
,react
根据这个来渲染产生浏览器的Dom
树。
react
在更新阶段对比原有的Vitural Dom
和新生成的Vitural Dom
,找出不同之处,在根据不同来渲染Dom
树。
react
为了追求高性能,采用了时间复杂度为O(N)
来比较两个属性结构的区别,因为要确切比较两个树形结构,需要通过O(N^3)
,这会降低性能。
- 节点类型不同
// A组件
<div>
<Todos />
</div>
// B组件
<span>
<Todos />
</span>
我们想把A
组件更新成B
组件,react
在做比较的时候,发现最外面的根结点完全不一样,直接销毁之前的<div>
节点,包括里面的子节点也一并销毁,这是一个巨大的浪费,但是为了避免O(N^3)
的时间复杂度,只能采用这种方式。
所以在开发过程中,我们应该尽量避免上面的情况,不要将包裹节点的类型随意改变。
- 两个节点类型一样
这里包括两种情况,一种是节点是Dom
类型,还有一种react
组件。
对于dom
类型,我们举个例子:
// A组件
<div style={{color: 'red',fontSize:15}} className="welcome">
Hello World!!!
</div>
// B组件
<div style={{color: 'green',fontSize:15}} className="react">
Good Bye!!!
</div>
上述A和B组件的区别是文字、className
、style
中的color
发生改变,因为Dom
元素没变,React
只会修改他变化的部分。
针对react
组件类型,渲染无非就是再执行一遍组件实例的更新过程,最主要的就是定制shouldComponentUpdate
。
- 多个子组件情况
例子一:
// A
<ul>
<TodoItem text="First" complete={false} />
<TodoItem text="Second" complete={false} />
</ul>
// B
<ul>
<TodoItem text="First" complete={false} />
<TodoItem text="Second" complete={false} />
<TodoItem text="Third" complete={false} />
</ul>
从A变到B,如果shouldComponentUpdate
处理得当,我们只需要更新装载third
的那一次就行。
我们来看看下一个例子:
// A
<ul>
<TodoItem text="First" complete={false} />
<TodoItem text="Second" complete={false} />
</ul>
// B
<ul>
<TodoItem text="Zero" complete={false} />
<TodoItem text="First" complete={false} />
<TodoItem text="Second" complete={false} />
</ul>
这里因为react
是采用O(n)
的时间复杂度,所以会依次将text
为First
的改为Zero
,text
为Second
改为First
,在最后再加上一个组件,text
为Second
。现存的两个的text
的属性都被改变了,所以会依次渲染。
如果我们这里有100个实例,那么就会发生100次更新。
这里我们就要用到Key
了
简单来说,其实这一个Key
就是react
组件的身份证号。
我们将上一个例子改成如下,就可以避免上面的问题了,react
就能够知道其实B里面的第二个和第三个组件其实就是A中的第一个和第二个实例。
// A
<ul>
<TodoItem key={1} text="First" complete={false} />
<TodoItem key={2} text="Second" complete={false} />
</ul>
// B
<ul>
<TodoItem key={0} text="Zero" complete={false} />
<TodoItem key={1} text="First" complete={false} />
<TodoItem key={2} text="Second" complete={false} />
</ul>
不过现在,react
也会提醒我们不要忘记使用key
,如果没有加,浏览器中会报错。
关于key
的使用我们要注意的是,这个key
值要稳定不变的,就如同身份证号对于我们是稳定不变的一样。
一个常见的错误就是,拿数组的的下标值去当做key
,这个是很危险的,代码如下,我们一定要避免
<ul>
{
todos.map((item, index) => {
<TodoItem
key={index}
text={item.text}
completed={item.completed}
>
})
}
</ul>
未完待续…