面试官:React怎么做性能优化
前言
最近一直在学习关于React方面的知识,并有幸正好得到一个机会将其用在了实际的项目中。所以我打算以博客的形式,将我在学习和开发(React)过程中遇到的问题记录下来。
这两天遇到了关于组件不必要的重复渲染问题,看了很多遍官方文档以及网上各位大大们的介绍,下面我会通过一些demo结合自己的理解进行汇总,并以此作为学习React的第一篇笔记(自己学习,什么都好,就是费头发...)。
本文主要介绍以下三种优化方式(三种方式有着相似的实现原理):
- shouldComponentUpdate
- React.PureComponent
- React.memo
其中shouldComponentUpdate
和React.PureComponent
是类组件中的优化方式,而React.memo
是函数组件中的优化方式。
引出问题
- 新建Parent类组件。
import React, { Component } from 'react'
import Child from './Child'
class Parent extends Component {
constructor(props) {
super(props)
this.state = {
parentInfo: 'parent',
sonInfo: 'son'
}
this.changeParentInfo = this.changeParentInfo.bind(this)
}
changeParentInfo() {
this.setState({
parentInfo: `改变了父组件state:${Date.now()}`
})
}
render() {
console.log('Parent Component render')
return (
<div>
<p>{this.state.parentInfo}</p>
<button onClick={this.changeParentInfo}>改变父组件state</button>
<br/>
<Child son={this.state.sonInfo}></Child>
</div>
)
}
}
export default Parent
- 新建Child类组件。
import React, {Component} from 'react'
class Child extends Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
console.log('Child Component render')
return (
<div>
这里是child子组件: <p>{this.props.son}</p>
</div>
)
}
}
export default Child
- 打开控制台,我们可以看到控制台中先后输出了
Parent Component render
和Child Component render
。 点击按钮,我们会发现又输出了一遍Parent Component render
和Child Component render
。 点击按钮时我们只改变了父组件Parent
state中的parentInfo
的值,Parent
更新的同时子组件Child
也进行了重新渲染,这肯定是我们不愿意看到的。所以下面我们就围绕这个问题介绍本文的主要内容。
shouldComponentUpdate
React提供了生命周期函数shouldComponentUpdate()
,根据它的返回值(true | false),判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染(这也就说明了上面👆Child组件重新渲染的原因)。
引用一段来自官网的描述:
当 props 或 state 发生变化时,
shouldComponentUpdate()
会在渲染执行之前被调用。返回值默认为 true。目前,如果shouldComponentUpdate
返回 false,则不会调用UNSAFE_componentWillUpdate()
,render()
和componentDidUpdate()
方法。后续版本,React 可能会将shouldComponentUpdate()
视为提示而不是严格的指令,并且,当返回 false 时,仍可能导致组件重新渲染。
shouldComponentUpdate
方法接收两个参数nextProps
和nextState
,可以将this.props
与nextProps
以及this.state
与nextState
进行比较,并返回 false 以告知 React 可以跳过更新。
shouldComponentUpdate (nextProps, nextState) {
return true
}
此时我们已经知道了shouldComponentUpdate
函数的作用,下面我们在Child
组件中添加以下代码:
shouldComponentUpdate(nextProps, nextState) {
return this.props.son !== nextProps.son
}
这个时候再点击按钮修改父组件 state 中的parentInfo
的值时,Child
组件就不会再重新渲染了。
这里有个注意点就是,我们从父组件Parent
向子组件Child
传递的是基本类型的数据,若传递的是引用类型的数据,我们就需要在shouldComponentUpdate
函数中进行深层比较。但这种方式是非常影响效率,且会损害性能的。所以我们在传递的数据是基本类型是可以考虑使用这种方式(即:this.props.son !== nextProps.son
)进行性能优化。
(关于基本类型数据和引用类型数据的介绍,可以参考一下这篇文章:传送门)
React.PureComponent
React.PureComponent
与React.Component
很相似。两者的区别在于React.Component
并未实现 shouldComponentUpdate
,而React.PureComponent
中以浅层对比 prop 和 state 的方式来实现了该函数。
将Child
组件的内容修改为以下内容即可,这是不是很方便呢。参考 前端进阶面试题详细解答
import React, { PureComponent } from 'react'
class Child extends PureComponent {
constructor(props) {
super(props)
this.state = {
}
}
render() {
console.log('Child Component render')
return (
<div>
这里是child子组件: <p>{this.props.son}</p>
</div>
)
}
}
export default Child
所以,当组件的 props 和 state 均为基本类型时,使用React.PureComponent
可以起到优化性能的作用。
如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。
为了更好的感受引用类型数据传递的问题,我们先改写一下上面的例子:
- 修改
Child
组件。
import React, {Component} from 'react'
class Child extends Component {
constructor(props) {
super(props)
this.state = {}
}
shouldComponentUpdate(nextProps, nextState) {
return this.props.parentInfo !== nextProps.parentInfo
}
updateChild () {
this.forceUpdate()
}
render() {
console.log('Child Component render')
return (
<div>
这里是child子组件: <p>{this.props.parentInfo[0].name}</p>
</div>
)
}
}
export default Child
- 修改
Parent
组件。
import React, { Component } from 'react'
import Child from './Child'
class Parent extends Component {
constructor(props) {
super(props)
this.state = {
parentInfo: [
{ name: '哈哈哈' }
]
}
this.changeParentInfo = this.changeParentInfo.bind(this)
}
changeParentInfo() {
let temp = this.state.parentInfo
temp[0].name = '呵呵呵:' + new Date().getTime()
this.setState({
parentInfo: temp
})
}
render() {
console.log('Parent Component render')
return (
<div>
<p>{this.state.parentInfo[0].name}</p>
<button onClick={this.changeParentInfo}>改变父组件state</button>
<br/>
<Child parentInfo={this.state.parentInfo}></Child>
</div>
)
}
}
export default Parent
此时在控制台可以看到,Parent
和Child
都进行了一次渲染,显示的内容是一致的。
点击按钮,那么问题来了,如图所示,父组件Parent
进行了重新渲染,从页面上我们可以看到,Parent
组件中的parentInfo
确实已经发生了改变,而子组件却没有发生变化。
所以当我们在传递引用类型数据的时候,shouldComponentUpdate()
和React.PureComponent
存在一定的局限性。
针对这个问题,官方给出的两个解决方案:
- 在深层数据结构发生变化时调用
forceUpdate()
来确保组件被正确地更新(不推荐使用); - 使用
immutable
对象加速嵌套数据的比较(不同于深拷贝);
forceUpdate
当我们明确知道父组件Parent
修改了引用类型的数据(子组件的渲染依赖于这个数据),此时调用forceUpdate()
方法强制更新子组件,注意,forceUpdate()
会跳过子组件的shouldComponentUpdate()
。
修改Parent
组件(将子组件通过ref暴露给父组件,在点击按钮后调用子组件的方法,强制更新子组件,此时我们可以看到在父组件更新后,子组件也进行了重新渲染)。
{
...
changeParentInfo() {
let temp = this.state.parentInfo
temp[0].name = '呵呵呵:' + new Date().getTime()
this.setState({
parentInfo: temp
})
this.childRef.updateChild()
}
render() {
console.log('Parent Component render')
return (
<div>
<p>{this.state.parentInfo[0].name}</p>
<button onClick={this.changeParentInfo}>改变父组件state</button>
<br/>
<Child ref={(child)=>{this.childRef = child}} parentInfo={this.state.parentInfo}></Child>
</div>
)
}
}
immutable
Immutable.js是 Facebook 在 2014 年出的持久性数据结构的库,持久性指的是数据一旦创建,就不能再被更改,任何修改或添加删除操作都会返回一个新的 Immutable 对象。可以让我们更容易的去处理缓存、回退、数据变化检测等问题,简化开发。并且提供了大量的类似原生 JS 的方法,还有 Lazy Operation 的特性,完全的函数式编程。
Immutable 则提供了简洁高效的判断数据是否变化的方法,只需 === 和 is 比较就能知道是否需要执行 render(),而这个操作几乎 0 成本,所以可以极大提高性能。首先将Parent
组件中调用子组件强制更新的代码this.childRef.updateChild()
进行注释,再修改Child
组件的shouldComponentUpdate()
方法:
import { is } from 'immutable'
shouldComponentUpdate (nextProps = {}, nextState = {}) => {
return !(this.props === nextProps || is(this.props, nextProps)) ||
!(this.state === nextState || is(this.state, nextState))
}
此时我们再查看控制台和页面的结果可以发现,子组件进行了重新渲染。
关于shouldComponentUpdate()
函数的优化,上面👆的方法还有待验证,仅作为demo使用,实际的开发过程中可能需要进一步的探究选用什么样的插件,什么样的判断方式才是最全面、最合适的。如果大家有好的建议和相关的文章欢迎砸过来~
React.memo
关于React.memo
的介绍,官网描述的已经很清晰了,这里我就直接照搬了~
React.memo
为高阶组件。它与React.PureComponent
非常相似,但只适用于函数组件,而不适用 class 组件。如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在
React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
React.memo
仅检查 props 变更。如果函数组件被React.memo
包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/* 如果把 nextProps 传入 render 方法的返回结果与 将 prevProps 传入 render 方法的返回结果一致则返回 true, 否则返回 false */
}
export default React.memo(MyComponent, areEqual)
使用函数组件改写一下上面的例子:
Child
组件:
import React, {useEffect} from 'react'
// import { is } from 'immutable'
function Child(props) {
useEffect(() => {
console.log('Child Component')
})
return (
<div>
这里是child子组件: <p>{props.parentInfo[0].name}</p>
</div>
)
}
export default Child
Parent
组件:
import React, {useEffect, useState} from 'react'
import Child from './Child'
function Parent() {
useEffect(() => {
console.log('Parent Component')
})
const [parentInfo, setParentInfo] = useState([{name: '哈哈哈'}])
const [count, setCount] = useState(0)
const changeCount = () => {
let temp_count = count + 1
setCount(temp_count)
}
return (
<div>
<p>{count}</p>
<button onClick={changeCount}>改变父组件state</button>
<br/>
<Child parentInfo={parentInfo}></Child>
</div>
)
}
export default Parent
运行程序后,和上面的例子进行一样的操作,我们会发现随着父组件count
的值的修改,子组件也在进行重复渲染,由于是函数组件,所以我们只能通过React.memo
高阶组件来跳过不必要的渲染。
修改Child
组件的导出方式:export default React.memo(Child)
。
再运行程序,我们可以看到父组件虽然修改了count
的值,但子组件跳过了渲染。
这里我用的是React hooks的写法,在hooks中useState
修改引用类型数据的时候,每一次修改都是生成一个新的对象,也就避免了引用类型数据传递的时候,子组件不更新的情况。
刚接触react,最大的感触就是它的自由度是真的高,所有的内容都可以根据自己的喜好设置,但这也增加了初学者的学习成本。(不过付出和收获是成正比的,继续我的救赎之路!)
总结
- 类组件中:
shouldComponentUpdate()
和React.PureComponent
在基本类型数据传递时都可以起到优化作用,当包含引用类型数据传递的时候,shouldComponentUpdate()
更合适一些。 - 函数组件:使用
React.memo
。
另外吐槽一下现在的网上的部分“博客”,一堆重复(一模一样)的文章。复制别人的文章也请自己验证一下吧,API变更、时代发展等因素引起的问题可以理解,但是连错别字,错误的使用方法都全篇照搬,然后文末贴一下别人的地址这就结束了???怕别人的地址失效,想保存下来?但这种方式不说误导别人,就说自己回顾的时候也会有问题吧,这是什么样的心态?
再说下上个月身边的真实例子。有个同事写了篇关于vue模板方面的博客,过了两天竟然在今日头条的推荐栏里面看到了一模一样的一篇文章,连文中使用的图片都是完全一样(这个侵权的博主是谁这里就不透露了,他发的文章、关注者还挺多,只能表示呵呵了)。和这位“光明磊落”的博主进行沟通,得到的却是:“什么你的我的,我看到了就是我的”这样的回复。真是天下之大,无奇不有,果断向平台提交了侵权投诉。然后该博主又舔着脸求放过,不然号要被封了,可真是好笑呢...(负能量结束明天又是美好的一天~)
这篇文章就先到这里啦,毕竟还处于自学阶段,很多理解还不是很全面,文中若有不足之处,欢迎各位看官大大们的指正