背景
在React中当组件的属性或者状态发生变化时,React 会调用组件的 render() 方法重新渲染组件。
以下是一些会导致组件重新渲染的情况:
- 组件的
props发生变化; - 组件的
state发生变化; - 父组件重新渲染;
- 使用
forceUpdate()强制重新渲染;
在 React 中,每次状态或属性变化时,组件都会重新渲染。如果一个组件引入很多子组件,当父组件状态变化,整体重新渲染就非常消耗性能。
一、React.memo()
概述
React.memo()是一个高阶组件,用于在某种特定的条件下优化React组件的性能。它类似于类组件的React.PureComponent。它接收一个函数组件,并返回一个新的组件。它的作用就是当父组件传给当前被memo()包裹的组件的props值没有发生变化时,该组件不会重新渲染。
用法
import React from 'react';
import 'antd/dist/antd.css';
import './index.css';
import { Input, Button, Space } from 'antd';
const App: React.FC = () => {
console.log('App Render');
const [value, setValue] = React.useState('');
const [list, setList] = React.useState([
{
name: '鸡腿堡',
id: 0,
},
{
name: '牛肉堡',
id: 1,
},
]);
const handleChange = (e) => {
const {
target: { value },
} = e;
setValue(value);
};
const handleAdd = () => {
setList([...list, { id: Number(new Date()), name: value }]);
};
return (
<div>
<Space>
<Input value={value} onChange={handleChange} placeholder="请输入" />
<Button type="primary" onClick={handleAdd}>
添加
</Button>
</Space>
<List list={list} />
</div>
);
};
const List = (props) => {
console.log('List Render');
const { list } = props;
return list.map((item) => (
<p style={{ marginTop: 10, padding: 10, background: 'pink' }} key={item.id}>
<Space>
{item.name}
<Button type="link">删除</Button>
</Space>
</p>
));
};
export default App;
在上面的代码中当我们改变输入框的值时,会触发state的变化,从而导致App组件重新渲染;然而List组件接收到App组件的list并无变化,也会导致重新渲染。如下图所示:
为了防止组件的频繁重新渲染,可以使用React.memo()将组件包裹,这样memo会判断如果组件没有props的变化时,不会重新渲染组件,代码如下:
// 给List组件添加memo包裹
const List = React.memo((props) => {
console.log('List Render');
const { list } = props;
return list.map((item) => (
<p style={{ marginTop: 10, padding: 10, background: 'pink' }} key={item.id}>
<Space>
{item.name}
<Button type="link">删除</Button>
</Space>
</p>
));
});
这样在App组件重新渲染时,List组件不会重新渲染,达到优化性能的目的。
注: 需要注意的是,React.memo()是对props的浅层比较,如果父组件传给子组件的是引用类型,只会比较它们的引用地址,不会进行深层次的比较。所以当我们传给子组件的是函数或者对象是,这时候就需要用到useCallback和useMemo进行优化;
使用React.memo并不是一定会提升性能,只有当组件的渲染成本比props比较成本高得多时,才会有明显的性能提升。如果组件的渲染成本很低,而props比较成本很高,那么使用React.memo反而会降低性能。
因此,在使用React.memo时,需要根据实际情况进行衡量,综合考虑组件的渲染成本和props比较成本,来判断是否使用React.memo。
二、React.useCallback()
概述
useCallback是React的一个Hook函数,用来缓存函数的引用,作用就是避免函数的重复创建
实际场景就是当父组件传给子组件一个函数时,父组件的渲染会造成该函数的重新创建,函数引用发生了变化,子组件判断props发生了变化导致子组件也重新渲染。
用法
useCallback 将一个函数和一个依赖项数组作为参数,当依赖项发生变化时,才会重新创建函数。否则,返回缓存的函数引用,这样就能避免不必要的函数创建和渲染。
// App组件
const handleRemove = (id) => {
setList([...list.filter((f) => f.id !== id)]);
};
<List list={list} remove={handleRemove} />
// List组件
const List = React.memo((props) => {
console.log('List Render');
const { list, remove } = props;
return list.map((item) => (
<p style={{ marginTop: 10, padding: 10, background: 'pink' }} key={item.id}>
<Space>
{item.name}
<Button type="link" onClick={() => remove(item.id)}>
删除
</Button>
</Space>
</p>
));
});
在上面的代码中,我们创建一个删除函数并传给子组件,由于函数是引用类型,在App组件重新渲染时,函数会重新创建,即便子组件有memo的包裹,函数引用的改变会造成props的变化,继而子组件重新渲染,如下图所示:
为了防止这种组件的重复渲染影响性能,这个时候就可以用到
useCallback,代码如下所示:
const handleRemove = React.useCallback(
(id) => {
setList([...list.filter((f) => f.id !== id)]);
},
[list] // 只有当依赖的list变化时,函数才会重新创建
);
这时,输入框变化时并不会造成子组件的更新,只有当我们添加元素或者删除元素时,函数引用才会变化,继而触发子组件的更新,如下图所示:
三、React.useMemo()
概述
useMemo用法和useCallback相似,都是用于缓存避免组件的重复渲染。它们的不同之处在于useMemo返回缓存的计算结果,而useCallback返回一个缓存的函数。
用法
缓存计算结果
当组件需要进行大量计算时,使用useMemo可以避免重复计算,提高组件的性能。
在上述例子中,我们给每一个汉堡添加价格;
const [list, setList] = React.useState([
{
name: '鸡腿堡',
id: 0,
price: 10,
},
{
name: '牛肉堡',
id: 1,
price: 20,
},
]);
// 计算总价
const total = () => {
console.log('我计算了!!!');
return list.reduce((prev, current) => {
return prev + current.price;
}, 0);
};
<div>总价:{total()}</div>
当我们输入框变化时,total函数会反复执行计算,返回相同的结果,如下图所示:
如果这个方法的计算量很大,会造成不必要的性能开销,我们需要的是当list变化时,才进行重新计算,这时useMemo就发挥作用,它在依赖项没有发生变化时,会缓存返回值,避免重复的重新计算,只有当添加或者删除汉堡时也就是依赖项改变时才会重新计算。
const total = React.useMemo(() => {
console.log('我计算了!!!');
return list.reduce((prev, current) => {
return prev + current.price;
}, 0);
}, [list]);
完整代码
import React from 'react';
import 'antd/dist/antd.css';
import './index.css';
import { Input, Button, Space } from 'antd';
const App: React.FC = () => {
console.log('App Render');
const [value, setValue] = React.useState('');
const [list, setList] = React.useState([
{
name: '鸡腿堡',
id: 0,
price: 10,
},
{
name: '牛肉堡',
id: 1,
price: 20,
},
]);
const handleChange = (e) => {
const {
target: { value },
} = e;
setValue(value);
};
const handleAdd = () => {
setList([...list, { id: Number(new Date()), name: value, price: 50 }]);
};
const handleRemove = React.useCallback(
(id) => {
setList([...list.filter((f) => f.id !== id)]);
},
[list] // 只有当依赖的list变化时,函数才会重新创建
);
const total = React.useMemo(() => {
console.log('我计算了!!!');
return list.reduce((prev, current) => {
return prev + current.price;
}, 0);
}, [list]);
return (
<div>
<Space>
<Input value={value} onChange={handleChange} placeholder="请输入" />
<Button type="primary" onClick={handleAdd}>
添加
</Button>
</Space>
<div>总价:{total}</div>
<List list={list} remove={handleRemove} />
</div>
);
};
const List = React.memo((props) => {
console.log('List Render');
const { list, remove } = props;
return list.map((item) => (
<p style={{ marginTop: 10, padding: 10, background: 'pink' }} key={item.id}>
<Space>
{item.name}
<Button type="link" onClick={() => remove(item.id)}>
删除
</Button>
</Space>
</p>
));
});
export default App;
四、总结
- memo:用于包裹组件,浅层比较其
props是否有变化而决定改组件是否需要重新渲染,用于优化性能。 - useCallback:用于缓存函数,避免函数的重复创建,提高组件的性能。
- useMemo:用于缓存计算结果,优化组件的性能。