使用纯组件提升渲染性能
一、组件的渲染浪费
有时组件的重新渲染不是必要的,这些渲染会影响应用程序的性能。举个例子:
下面组件的初始状态为 {count: 0}
,当单击 click Me 按钮时,它将 count
状态设置为 1。再次单击该按钮时组件不应该重新渲染,因为状态没有更改,count
的值还是 1。
import React from 'react'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate')
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate')
}
render() {
return (
<div>
{this.state.count}
<button onClick={() => this.setState({ count: 1 })}>Click Me</button>
</div>
)
}
}
export default App
上面组件的点击效果如下:
这里添加了两个生命周期方法来检测组件是否重新渲染,可以看到状态没有更改,但组件仍重新渲染,这就是组件的渲染浪费。
二、纯组件的实现
1、shouldComponentUpdate
shouldComponentUpdate 生命周期方法可以用于模拟纯组件,避免 React 组件中的渲染浪费。下面使用 shouldComponentUpdate 改写上面的组件:
import React from 'react'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate')
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate')
}
shouldComponentUpdate(nextProps, nextState) {
if (this.state.count === nextState.count) {
return false
}
return true
}
render() {
return (
<div>
{this.state.count}
<button onClick={() => this.setState({ count: 1 })}>Click Me</button>
</div>
)
}
}
export default App
点击效果如下:
这里用 shouldComponentUpdate 检查了当前状态对象 this.state.count 中的计数值是否等于下一个状态 nextState.count 对象的计数值。如果相等,则不应该重新渲染,因此返回 false
,反之返回 true
,从而避免不必要的渲染。
2、React.PureComponent
React v15.5 引入了 PureComponent,该组件默认启用更改检测,会自己判断是否需要重新渲染,不必再引入 shouldComponentUpdate 来进行前后状态判断,使用时只需要继承 React.PureComponent
即可。
下面使用 React.PureComponent
改写上面的组件:
import React from 'react'
class App extends React.Component {
class App extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate')
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate')
}
render() {
return (
<div>
{this.state.count}
<button onClick={() => this.setState({ count: 1 })}>Click Me</button>
</div>
)
}
}
export default App
点击效果如下:
使用 React.PureComponent
有几个需要注意的点:
2-1、不能再重写 shouldComponentUpdate
继承 React.PureComponent
时,如果再重写 shouldComponentUpdate,会引发警告。
2-2、引用类型浅比较机制
继承 React.PureComponent
时,进行的是浅比较,如果是引用类型的数据,只比较是否为同一个地址,而不比较具体数据是否完全一致。
举个例子,实现一个点击按钮叠加计时器。
下面是没使用 PureComponent 前:
import React from 'react'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
counts: [1]
}
}
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate')
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate')
}
handleClick() {
const newCounts = this.state.counts
const lastNum = this.state.counts[this.state.counts.length - 1]
newCounts.push(lastNum + 1)
this.setState({ counts: newCounts })
}
render() {
return (
<>
<div>{this.state.counts.join(' ')}</div>
<button onClick={() => this.handleClick()}>Add Num</button>
</>
)
}
}
export default App
点击效果如下:
使用 PureComponent 后:
class App extends React.Component {
class App extends React.PureComponent {
点击效果如下:
可以看到,使用 PureComponent 后无论怎么点击按钮,组件都不会重新渲染,原因是组件的状态值引用类型 counts 的引用地址始终是同一个。如果想让它变化,只需将 const newCounts = this.state.counts
改为 const newCounts = this.state.counts(0)
即可,因为改变了引用地址。
以上就是类组件优化成纯组件的写法,下面是在函数组件中实现纯组件。
2-3、PureComponent 源码分析
function ComponentDummy() {}
// ComponentDummy 的原型继承 Component 的原型
ComponentDummy.prototype = Component.prototype
function PureComponent(props, context, updater) {
// 构造函数属性(实例属性),会被实例共享,但不会被修改
this.props = props
this.context = context
this.refs = emptyObject
this.updater = updater || ReactNoopUpdateQueue
}
// 不能直接继承 Component
// 原因:直接继承 Component 还会继承它的 Constructor 方法
var pureComponentPrototype = (PureComponent.prototype = new ComponentDummy())
// 实例沿着原型链向上查询,只要是自己继承的,都被认作自己的构造函数
pureComponentPrototype.constructor = PureComponent
// 这里做了优化,把 Component.prototype 属性浅拷贝到 pureComponentPrototype 上
// 防止原型连拉长,导致方法的多层寻找,减少查询次数
_assign(pureComponentPrototype, Component.prototype)
// 添加 isPureReactComponen 来判断是 Component 还是 PureComponent 组件
pureComponentPrototype.isPureReactComponent = true
2-4、PureComponent 的更新机制
PureComponent 是如何实现是否需要更新来提高性能的呢?
// shouldUpdate 变量用来控制组件是否需要更新,默认为 true
var shouldUpdate = true
// inst 是组件实例
// 如果 PureComponent 定义有 shouldComponentUpdate 方法,则与 Component 基类一样
if (inst.shouldComponentUpdate) {
shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext)
} else {
if (this._compositeType === CompositeType.PureClass) {
// shallowEqual 比较值是否相等、或者对象是否含有相同的属性且属性值都相等
// 用 shallowEqual 函数对比 props 和 state 的改动
// 如果都没改变就不用更新
shouldUpdate =
!shallowEqual(prevProps, nextProps) ||
!shallowEqual(inst.state, nextState)
}
}
3、React.memo
3-1、什么是 React.memo
类组件可以使用 shouldComponentUpdate 或 PureComponent 避免渲染的浪费。而对于函数组件,React 提供了 React.memo 这个高阶组件(HOC),如果组件在 props 改变的情况下渲染相同的结果,则可以将其包装在 React.memo
中调用,React 将跳过渲染并直接复用最近一次渲染的结果,以避免渲染的浪费,从而提升性能。
React.memo
与 PureComponent 相似,但只适用于函数组件,而且 React.memo()
支持指定参数,相当于 shouldComponentUpdate 的作用,因此 React.memo()
用法更加灵活方便。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
nextProps 传入 render 方法的返回结果
prevProps 传入 render 方法的返回结果
二者相同则返回 true,否则返回 false
这里与 shouldComponentUpdate 的效果相反
*/
}
export default React.memo(MyComponent, areEqual)
最终 export 的组件,就是 React.memo()
包装之后的组件。
3-2、React.memo 的使用
举个例子,父组件传给子组件的 props 只要改变,无论子组件有没有用到这个 props,都会重新渲染:
import React, { useState } from 'react'
const Counter = (props) => {
console.log('render')
return <div>count is {props.count}</div>
}
const App = () => {
const [count, setCount] = useState(0)
const [noUsed, setNoUsed] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>Add Count</button>
<button onClick={() => setNoUsed(noUsed + 1)}>Add NoUsed</button>
<Counter count={count} noUsed={noUsed} />
</>
)
}
export default App
上面点击 Add NoUsed 按钮,子组件中没有用到这个值,但由于 props 发生变化,子组件还是会重新渲染:
而 React.memo()
就是用来优化这类问题的,优化如下:
import React, { useState } from 'react'
const Counter = (props) => {
console.log('render')
return <div>count is {props.count}</div>
}
const isEqual = (prevProps, nextProps) => {
if (prevProps.noUsed !== nextProps.noUsed) {
return true
}
return false
}
const MemoCounter = React.memo(Counter, isEqual)
const App = () => {
const [count, setCount] = useState(0)
const [noUsed, setNoUsed] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>Add Count</button>
<button onClick={() => setNoUsed(noUsed + 1)}>Add NoUsed</button>
<Counter count={count} noUsed={noUsed} />
<MemoCounter count={count} noUsed={noUsed} />
</>
)
}
export default App
优化后效果如下:
可以看到,props.noUsed
的值发生变化后,子组件不会重新渲染。
上面 React.memo
的使用都是在最外层包装了整个组件,并且需要手动写一个方法来比较具体的 props。而有时只希望组件的部分不重新渲染,而非整个组件不重新渲染(即实现局部 Pure 功能),可以用 useMemo() 进行细粒度性能优化。