一、React的渲染机制
要掌握一两项React-render优化的方法不难,但是非常重要.无论是在实际项目中的一个小细节,还是迎合'面试官'的口味
1.1 触发Render
我们知道React要更新视图,必须要触发Render.而这往往是影响性能最重要的一步(因为操作了dom).而React之所以这么出色,正是因为占其主导地位的diff算法采用了虚拟DOM(React V-dom),使得渲染性能大大提升。
即便如此,我们在开发的时候也应该要注意一些性能的优化,比如避免无意义的render
那么,触发render的条件有哪些呢?
首次加载组件
使用了setState(更新了Props)
Props更新了(父级传给子级的值改变了)
我们完全可以避免2.3情况导致的一些性能问题
1.2 React Diff
虽然说React高效的Diff算法完美结合了虚拟DOM,让用户可以'无限制'刷新而不需要考虑任何性能问题. 但diff算法还是需要一定的时间,如果你不在意触发render的细节,项目模块大了起来,自然就会影响性能了.
1.3 不做任何优化的例子
我尝试着从实现以下功能:
不更新state的值,但是调用了
setState
方法传给子级的值不改变,即子级props实际上是没变化的
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n31" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">// 父级.js
import React from 'react'
import MockChild from './child/index.js'
export default class Demo5 extends React.Component{
constructor(args){
super(args)
this.state = {
'mockNum': 0
}
}
handleClick(){
this.setState({
'mockNum': 0,
})
}
render(){
console.log('父级state ==============>', this.state)
return(
<div>
<button onClick={this.handleClick.bind(this)}>点击</button>
<MockChild mockNum={this.state.mockNum}/>
</div>
)
}
}
//子组件
render(){
console.log('子级Props =============>', this.props)
return(
<div></div>
)
}</pre>
我们反复点击按钮,虽然state的值并没有任何变化,但是我们看打印的结果!
render重复渲染了!
可能会疑问,我在项目并不会做这么一种无用功的!但实际上,当一个组件逻辑复杂起来之后,会产生各种各样的情况.比如:
比如一个组件的中有个包含onChange事件的input
,当我改变该输入框内容的时候调用了setState
,渲染视图的时候也会触发子组件的重新render.但我们明明没有做任何和子组件有联系的操作 例如上面的例子:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n37" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">//父组件.js
state = {
'mockValue': '123'
}
handleChange(e){
this.setState({
'value': '123',
})
}
//render
<input onChange={this.handleChange.bind(this)} />
<MockChild />
/*
- 子组件不做变化
*/</pre>
很不爽,真的!必须给安排掉.
二、基础的React优化
2.1 生命周期: shouldComponentUpdate
可能没听过shouldComponentUpdate
,我们简单介绍一下它的执行周期
不熟悉React生命周期的可以看看这篇文章:《React生命周期执行顺序详解》
可以看到它是在render渲染之前触发的,只要我们在这里加以判断就可以有效阻止'无用'的render触发.当然你说componentWillReceiveProps
也可以,当然!但是它只有props的更新判断,而有时候我们也不能放过未更改的state!
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n52" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">shouldComponentUpdate(nextProps, nextState){
if(nextState !== this.state && nextProps !== this.props) return true;
return false
}</pre>
这么简短的代码就可以解决冗余的render触发.当然有时候项目规模大了之后,就需要具体到某一个值
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n54" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">nextState.xxx !== this.state.xxx && ...</pre>
2.2 PureComponent
先介绍
PureComponent
的用法,实在是太简便了
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n61" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">import React, { PureComponent } from 'react'
export default class Demo5 extends PureComponent{
}</pre>
实际上就是把React.Component代替成PureComponent.
可能会疑问那PureComponent应该是一个插件吧?为什么就在react包里?其实它是官方在React15.3提出的一个'纯净的组件'
在版本允许的情况下,还是建议使用PureComponent,既能优化render次数,也能减少shouldComponentUpdate的代码。但是PureComponent只是执行了浅比较
什么是浅比较呢?
我们先来看看源码
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n67" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps)
|| !shallowEqual(inst.state, nextState);
}</pre>
可以看到判断主要是通过shallowEqual
方法执行的(即可以判断state,也可以判断props)
shallowEqual
具体作用是什么呢?实际上它使用了ES6的Object.keys
.只是做了以下这么几个判断:
新的和旧的props(state)是否两者都有相同的key?
引用是否改变
第二种可能有点难以理解,什么是引用是否改变?
简单地解释就是,是否有新的元素参与改变
举个官方常用例子:
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n78" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class App extends PureComponent {
state = {
items: [1, 2, 3]
}
handleClick = () => {
const { items } = this.state;
items.pop();
this.setState({ items });
}
render() {
return (<div>
<ul>
{this.state.items.map(i => <li key={i}>{i}</li>)}
</ul>
<button onClick={this.handleClick}>delete</button>
</div>)
}
}</pre>
可以看到,我们点击delete的时候,虽然items数组执行了pop()
方法,删除最后一项!但是li
标签却没变少!那是因为shallowEqual
根本不吃你这套.它认为items还是同一个引用,所以给我true
!在通过!反过来就是false
了
那要如果改变引用呢? 我们可以这样尝试.(这也是解决浅比较常用的一个办法)
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n81" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">this.setState({
items: [].concat(items)
});</pre>
当然,大多数情况我们都可以通过PureComponent解决啦!实在不行,还可以通过componentWillReceiveProps
进一步判断呢!
PureComponent
更多的介绍可以看:《React PureComponent 使用指南》