React - 渲染性能优化

总结避免子组件不必要渲染的一些方法。

水文预警:看题目就知道,这篇是水文,写给自己的备忘录。

React 中,父组件一旦渲染,子组件也会跟着重新渲染。(父债子还)

试想在复杂业务场景下,一群子组件依赖于父组件,然后父组件有逻辑触发了渲染,一群子组件跟着渲染,这不是炸了。(杀鸡用牛刀)

所以 React 性能优化的要素就是打断父子组件之间的强依赖关系,跳过不必要的重新渲染。但是当属性更新时,子组件又能进行及时响应并渲染。(指哪打哪)

本文讨论在子组件控制重新渲染的一些方法。

shouldComponentUpdate

shouldComponentUpdate 是一个生命周期,在组件更新阶段中触发,用于判断组件是否需要渲染。

组件更新:componentWillReceiveProps(接收属性) -> shouldComponentUpdate(判断是否渲染) -> componentWillUpdate -> render(重新渲染) -> componentDidUpdate

1
2
3
4
5
6
7
8
9
10
11
12
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.user === this.props.user) { //属性没有改变
return false; //不渲染
} else {
return true; //渲染
}
}

//简写
shouldComponentUpdate(nextProps, nextState) {
return nextProps !== this.props;
}

该生命周期方法接收两个变量,表示下一次渲染后的属性和变量;返回一个布尔值表示是否需要渲染。

在方法中我们可以对 nextProps(更新后属性)this.props (当前属性)nextState(更新后状态)this.state (当前状态) 进行一些判断,来决定是否需要重新渲染。当父组件传给子组件的属性没有发生变化时,子组件自然就不用重新渲染了,就达到了性能优化的效果。

但是需要注意的是,深比较(递归遍历对象下面的所有属性)可能得不偿失,一套比较下来还要渲染,还不如直接渲染来的快。

所以正确的食用方法如下:

  • 只进行浅比较,对比引用对象即可:nextProps === this.props
  • 进行深层次控制,比如只对比其中一部分属性:nextProps.username === this.props.username
  • 将对象转化为 immutable 对象进行比较。但是 jsimmutable 再转回 js 太麻烦了

immutable.js 是不可变数据集合,数据一旦创建就不能被修改,可以进行高效的惰性比较。

PureComponent

每个子组件都写一通 shouldComponentUpdate 也太麻烦了,所以 React 为我们准备了 PureComponent 组件,原型对象中默认实现了 浅比较

By the way,若是在 PureComponent 使用了 shouldComponentUpdate ,该方法会被重写。

1
2
3
4
5
6
7
8
9
import React, {PureComponent} from 'react';

export default class Child extends PureComponent{ //默认进行浅比较,决定是否重渲染
render() {
return (
...
)
}
}

上述两种方式只适用于 对象式组件,对于 函数式组件 需要使用以下方法:

memo

React.memo() 是一个 HOC(高阶组件),传入两个参数:函数式组件、判断方法,返回被包裹后的对象。通过判断方法中返回的结果来决定是否需要重新渲染。

第二个参数也可以不传,默认就是浅比较。

1
2
3
4
5
6
7
8
9
10
11
import React, { memo } from 'react';

const isEqual = (prevProps, nextProps) => {
return prevProps !== nextProps;
}

export default memo((props = {}) => {
return (
...
);
}, isEqual);

使用 Hooks 特性后,React.memo() 方法就失效了。因为每次渲染返回的都是一个新闭包,不管怎么比较都是重新渲染。

贴心的 React 团队提供了以下两个 Hooks 方法:

useMemo

useMemo 接受两个参数:函数 和 依赖项数组,返回缓存 memoized 值(个人理解是包装后的对象)。只有当依赖性数组中的某个值发生变化时,该函数才会重新渲染。

useMemo 的控制粒度更细,控制的是传入的函数,而不是整个组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState, useMemo } from 'react';

export default Father = (props = {}) => {
const [username, setUsername] = useState('zhaoo');
return (
<>
<Child username={username} />
<button onClick={()=>setUsername(username + 'o')}>改变用户名</button>
</>
)
}

export default Child = (props = {}) => {
console.log('--- component re-render ---'); //组件不会重新渲染
const {username} = props;
return useMemo(() => {
console.log('--- useMemo re-render ---'); //函数重新渲染
return (
<div>{username}</div>
)
}, [username]);
}

useCallback

useCallbackuseMemo 类似,也接受两个参数:回调函数 和 依赖项数组,返回缓存 memoized 值。当依赖性数组中的某个值发生变化时,传入的回调函数被新的闭包函数取代。

说白了就是,useMemo 中的函数直接运行,useCallback 返回的函数需要你手动跑。类比于 callbind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useCallback } from 'react';

export default Father = (props = {}) => {
const [username, setUsername] = useState('zhaoo');
const handleClick = useCallback(() => {
setUsername(username + 'o');
});
return (
<>
<Child username={username} />
<button onClick={handleClick}>改变用户名</button>
</>
)
}

export default Child = (props = {}) => {
const {username} = props;
return (
<div>{username}</div>
)
}

总结

上文只是对这些 API 过了一遍,还是乱讲的那种。不过产生了一些体会,只可意会不可言传。

生命周期Hooks,从 对象式组件函数式组件,从 命令式声明式,应该是思想上转变,以及为什么要引入 Hooks

而不是讨论 useEffect 中传什么参数可以模拟 componentDidMount 还是 componentWillUnmount 还是 componentWillUpdate

查看评论