前端小白学 React 框架(十一)
React性能优化
React的更新流程
React在props或state发生改变时,会调用React的render方法,会创建一棵不同的树。React需要基于这两棵不同的树之间的差别来判断如何有效的更新UI,如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度都会非常高,具体可参考这篇论文,如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围,这个开销太过昂贵了,React的更新性能会变得非常低效。于是,React对这个算法进行了优化,将其优化成了O(n):
- 同层节点之间相互比较,不会垮节点比较;
- 不同类型的节点,产生不同的树结构;
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定(尽量不更新)。
keys的优化
一般情况下不写key的话会在控制台看到这样的报错:
- 在最后位置插入数据,这种情况,有无key意义并不大;
- 在前面插入数据,这种做法,在没有key的情况下,所有的li都需要进行修改;
- key的注意事项:
- key应该是唯一的;
- key不要使用随机数 (随机数在下一次render时,会重新生成一个数字)
- 使用index作为key,对性能是没有优化的;
SCU优化
看个小例子🌰,有如下代码:
import React, { Component } from 'react';
import Home from './Home.jsx';
import Recommand from './Recommand.jsx';
export class App extends Component {
constructor() {
super();
this.state = {
message: 'hello world',
count: 0
}
}
changeText() {
this.setState({ message: 'hello react' });
}
render() {
console.log('App render');
const { message, count } = this.state;
return (
<div>
<h1>render函数的优化</h1>
<h2>App-{message}-{count}</h2>
<button onClick={() => this.changeText()}>修改文本</button>
<Home />
<Recommand />
</div>
)
}
}
export default App
点击按钮后,文本会更改,但是Home
与Recommand
组件没有更改,因此我们想着应该不应该调用render函数,但是事实真实如此嘛?看一下实际的案例:
可以发现,Home
与Recommand
组件的render函数还是被调用了,这样就造成了性能的浪费。甚至运行this.setState({ message: 'hello world' });
也会重新调用render函数。
解决方法
那么,我们可以思考一下,在以后的开发中,我们只要是修改了App中的数据,所有的组件都需要重新render,进行diff算法性能必然是很低的,事实上,很多的组件没有必须要重新render;它们调用render应该有一个前提,就是依赖的数据 (state、props)发生改变时,再调用自己的render方法。
那么就可以通过shouldComponentUpdate
方法来控制render方法是否被调用。
shouldComponentUpdate
React给我们提供了一个生命周期方法 shouldComponentUpdate (我们简称为SCU),这个方法接受参数,并且需要有返回值,该方法有两个参数:
- 参数一:
nextProps
修改之后,最新的props属性; - 参数二:
nextState
修改之后,最新的state属性。
该方法返回值是一个boolean类型:
- 返回值为true,那么就需要调用render方法;
- 返回值为false,那么就不需要调用render方法;
- 默认返回的是true,也就是只要state发生改变,就会调用render方法。
比如我们在App中增加一个message属性,jsx中并没有依赖这个message,那么它的改变不应该引起重新渲染,但是因为render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了。
具体的使用看如下例子:
import React, { Component } from 'react'
export class Home extends Component {
shouldComponentUpdate() {
return false;
}
render() {
console.log('Home render');
return (
<div>Home</div>
)
}
}
export default Home
但是直接return false
会有一个缺点,就是如果真的有数据要更改,那么也不会发生改变,因此需要自己写判断逻辑控制组件是否调用render函数。
那么我们现在实现一个需求:如果更改的内容与以前不一样就调用render,否则不调用,来看个例子:
import React, { Component } from 'react';
import Home from './Home.jsx';
import Recommand from './Recommand.jsx';
export class App extends Component {
constructor() {
super();
this.state = {
message: 'hello world',
count: 0
}
}
changeText() {
this.setState({ message: 'hello world' });
}
changeText2() {
this.setState({ message: 'hello react' });
}
shouldComponentUpdate(newProps, newState) {
return this.state.message !== newState.message;
}
render() {
console.log('App render');
const { message, count } = this.state;
return (
<div>
<h1>render函数的优化</h1>
<h2>App-{message}-{count}</h2>
<button onClick={() => this.changeText()}>修改相同的文本</button>
<button onClick={() => this.changeText2()}>修改不同的文本</button>
<Home />
<Recommand />
</div>
)
}
}
export default App
以下是运行结果:
💡 子组件不仅要对比自己的
state
还要对比传过来的props
。
如果所有的类,我们都需要手动来实现 shouldComponentUpdate
,那么会给我们开发者增加非常多的工作量,我们来设想一下shouldComponentUpdate
中的各种判断的目的是什么?props或者state中的数据是否发生了改变,来决定shouldComponentUpdate
返回true或者false。
🎉 好消息!!!
🎉 好消息!!!
🎉 好消息!!!
事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?将class继承自PureComponent。
PureComponent
示例代码如下:
import React, { PureComponent } from 'react';
// ...
export class App extends PureComponent {
// ...
// shouldComponentUpdate(newProps, newState) {
// return this.state.message !== newState.message;
// }
render() {
// ...
}
}
大家可以去控制台看打印结果,发现可以实现上面SCU优化的效果,但是PureComponent只能进行浅层比较。
那么如果是函数式组件也想要实现这样的功能呢?React也提供了memo函数。
memo
export default function Profile(props) {
console.log('Profile render');
return <h2>Profile: {props.message}</h2>
}
如果点击+1
按钮,但是Profile组件没有依赖count,但是也会发生更改,如果没有加memo
的效果是这样的:
因此为了解决这个问题,就需要用到memo
了:
import { memo } from "react";
const Profile = memo(function (props) {
console.log('Profile render');
return <h2>Profile: {props.message}</h2>
});
export default Profile;
加memo
的效果是这样的:
不可变数据的力量
看下面的例子:
import React, { PureComponent } from 'react'
export class App extends PureComponent {
constructor() {
super();
this.state = {
books: [
{ name: 'book1', price: 90, count: 1 },
{ name: 'book2', price: 49, count: 2 },
{ name: 'book3', price: 59, count: 4 },
{ name: 'book4', price: 99, count: 1 },
]
}
}
addBook() {
const newBook = { name: 'book5', price: 19, count: 6 };
this.state.books.push(newBook);
this.setState({
books: this.state.books
})
}
render() {
return (
<div>
<h1>数据不可变的力量</h1>
<ul>
{
this.state.books.map((book, index) => {
return (
<li key={index}>
<span>{book.name}-{book.price}-{book.count}</span>
<button>+1</button>
</li>
)
})
}
</ul>
<button onClick={() => this.addBook()}>添加新书</button>
</div>
)
}
}
export default App
点击添加书籍按钮会发现页面不会发生变化,这是因为我们继承的是PureComponent
,每次进行setState操作的时候会对比两个变量是否发生改变,而对象存储的是内存地址,只使用push操作是不会重新将state里的变量赋值为新地址,因此就不会发生改变。
如何解决这个问题呢?只要将原数组浅拷贝一份就可以了,这样子页面就能更新了:
addBook() {
const newBook = { name: 'book5', price: 19, count: 6 };
const books = [...this.state.books, newBook];
this.setState({ books });
}
效果如下:
其他详情可以查看🔎官方文档Optimizing Performance。
PureComponent更新操作的源码
function checkShouldComponentUpdate(
workInProgress: Fiber,
ctor: any,
oldProps: any,
newProps: any,
oldState: any,
newState: any,
nextContext: any,
) {
const instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === 'function') {
let shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
if (__DEV__) {
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
) {
setIsStrictModeForDevtools(true);
try {
// Invoke the function an extra time to help detect side-effects.
shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
} finally {
setIsStrictModeForDevtools(false);
}
}
if (shouldUpdate === undefined) {
console.error(
'%s.shouldComponentUpdate(): Returned undefined instead of a ' +
'boolean value. Make sure to return true or false.',
getComponentNameFromType(ctor) || 'Component',
);
}
}
return shouldUpdate;
}
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true;
}
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
// $FlowFixMe[incompatible-use] lost refinement of `objB`
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
return true;
}
可以看出来,如果PureComponent
中写了shouldComponentUpdate
函数还是会执行的,并且优先级比源码的shallowEqual
高,但是一般PureComponent
中不写shouldComponentUpdate
。
写在最后
如果大家喜欢的话可以收藏本专栏,之后会慢慢更新,然后大家觉得不错可以点个赞或收藏一下 🌟。
博客内的项目源码在react-app
分支,大家可以拷贝下来。
转载自:https://juejin.cn/post/7235259315589759032