关键词: shouldComponentUpdate、PureComnent、React.memo、useMemo、useCallback
shouldComponentUpdate 与 PureComnent
shouldComponentUpdate
与 PureComnent
用于类组件。虽然官方推荐使用函数组件,但我们依然需要对类组件的渲染优化策略有所了解,不仅是维护旧的类组件代码需要,很多优化的概念是通用的。
所以,我们先简单了解一下 shouldComponentUpdate
与 PureComnent
。
先看一个类组件示例
import React from 'react';
class Child extends React.Component {
render() {
console.log('Child rendered');
return (
<div>
<h1>Child Count: {this.props.count}</h1>
</div>
);
}
}
class App extends React.Component {
state = {
count: 0,
otherValue: 'Hello',
};
increment = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
changeOtherValue = () => {
this.setState({ otherValue: this.state.otherValue === 'Hello' ? 'World' : 'Hello' });
};
render() {
console.log('Parent rendered');
return (
<div>
<h1>otherValue: {this.state.otherValue}</h1>
<Child count={this.state.count} />
<button onClick={this.increment}>Increment Count</button>
<button onClick={this.changeOtherValue}>Change Other Value</button>
</div>
);
}
}
export default App;
在上面的代码中,Child 组件的 count
属性是 App 的 state 的一部分。点击 APP 组件的 Increment Count
,count 会增加,此时 App 组件重新渲染了,Child 组件也重新渲染了:
点击 APP 组件的 Change Other Value
,otherValue 会改变,此时 App 组件重新渲染了,但 Child 组件虽然没有用到 otherValue
,但依然重新渲染了:
这是因为当 Parent 组件(在这个案例中是 App)的 state
或 props
发生变化时,React 会默认重新渲染该组件及其所有 Child 组件。
此时就可以用到shouldComponentUpdate
来优化性能,避免不必要的渲染。
shouldComponentUpdate
文档:https://zh-hans.react.dev/reference/react/Component#shouldcomponentupdate
shouldComponentUpdate
是一个生命周期方法,可以用来决定组件是否需要更新。返回 true
会让组件继续更新,而返回 false
则会阻止更新。
使用 shouldComponentUpdate
优化后的 Child 代码如下:
class Child extends React.Component {
shouldComponentUpdate(nextProps) {
// 仅在 count 属性变化时重新渲染
return this.props.count !== nextProps.count;
}
render() {
console.log('Child rendered');
return (
<div>
<h1>Child Count: {this.props.count}</h1>
</div>
);
}
}
此时,点击 APP 组件的 Change Other Value
,otherValue
会改变,但 Child 组件不会重新渲染:
PureComponent
除了手动实现 shouldComponentUpdate
,我们还可以使用 React.PureComponent来自动处理这一逻辑。PureComponent
会对其 props
进行浅比较
,如果 props 没有变化,则不会重新渲染。
下面是使用 PureComponent
重写 Counter 组件的示例:
class Child extends React.PureComponent {
render() {
console.log('Child rendered');
return (
<div>
<h1>Child Count: {this.props.count}</h1>
</div>
);
}
}
使用 PureComponent 后,Child 组件在 props.count
没有变化时将也不会重新渲染。
需要注意的是,PureComponent 并未实现 shouldComponentUpdate()。
React.PureComponent 只进行浅比较,如果 props 或 state 中包含复杂的数据结构(如对象或数组),浅比较可能无法正确判断数据是否发生变化。在这种情况下,可以使用深比较或手动实现 shouldComponentUpdate
来确保组件正确地更新。(但其实我们一般在更新数组时都是返回一个新的数组从而改变引用地址)。
React.memo
文档:https://zh-hans.react.dev/reference/react/memo
其实在官方文档中,shouldComponentUpdate 和 PureComponent 都被列为了过时的 API,官方推荐使用 React.memo
来代替。
React.memo 是一个高阶组件,类似于 PureComponent
,但其使用于函数组件。它接受一个函数组件作为参数,并返回一个新的函数组件。新的函数组件会对传入的 props 进行浅比较来决定是否重新渲染组件。
把上面的组件改成函数组件,并在 Child 组件使用 React.memo
:
import React, { useState } from 'react';
// 将 Child 组件定义为函数组件并使用 React.memo
const Child = React.memo(({ count }) => {
console.log('Child rendered');
return (
<div>
<h1>Child Count: {count}</h1>
</div>
);
});
const App = () => {
const [count, setCount] = useState(0);
const [otherValue, setOtherValue] = useState('Hello');
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
const changeOtherValue = () => {
setOtherValue((prevValue) => (prevValue === 'Hello' ? 'World' : 'Hello'));
};
console.log('Parent rendered');
return (
<div>
<Child count={count} />
<button onClick={increment}>Increment Count</button>
<button onClick={changeOtherValue}>Change Other Value</button>
</div>
);
};
export default App;
可以看到,使用 React.memo
可以和 PureComponent
一样,当 props.count
没有变化时,Child 组件不会重新渲染。
前面说到 React.memo
是一个高阶组件。实际上, React.memo 的源码就是返回一个具有类似于 PureComponent 的行为的组件。
需要注意的是,React.memo
也是只对 props 进行浅比较
。
那么,如果 Child 组件的 props 中包含复杂的数据结构,我们在更新时习惯性地返回一个新的对象或数组,就能避免浅比较的问题。
React.memo 语法
除此之外,React.memo
还可以接受第二个参数,用于自定义比较逻辑。第二个参数是一个函数,接受两个参数:oldProps
和 newProps
,返回一个布尔值,表示是否需要重新渲染组件。
function MyComponent(props) {
/* 使用 props 渲染 */
}
export default React.memo(MyComponent, areEqual);
// 自定义比较逻辑
function areEqual(oldProps, newProps) {
// 在这里自定义规则
// 如果返回true,表示新旧props相等,不渲染 与shouldComponentUpdate相反
// 如果返回false,表示新旧props不等,重新渲染
}
useCallback
useCallback
是一个 React Hook
,用于优化函数组件的性能。具体的作用简单来说就是缓存函数。
文档:https://zh-hans.react.dev/reference/react/useCallback
仅使用 React.memo 时遇到的问题
在实际开发时,在一个组件中会出现很多 Child 组件。我们还是以之前的例子为例,把 count
和 increment
放到 Child 组件中:
import React, { useState } from 'react';
// 将 Child 组件定义为函数组件并使用 React.memo
const Child = React.memo(() => {
console.log('Child rendered');
const [count, setCount] = useState(0);
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
return (
<div style={{ border: '1px solid black', width: '300px', padding: '10px' }}>
<h1>Child Count: {count}</h1>
<button onClick={increment}>Increment Count</button>
</div>
);
});
const App = () => {
const [otherValue, setOtherValue] = useState('Hello');
const changeOtherValue = () => {
setOtherValue((prevValue) => (prevValue === 'Hello' ? 'World' : 'Hello'));
};
console.log('Parent rendered');
return (
<div>
<h1>otherValue: {otherValue}</h1>
<button onClick={changeOtherValue}>Change Other Value</button>
<Child />
</div>
);
};
export default App;
分别点击 Increment Count
按钮和 Change Other Value
按钮,可以看到,各自的更新没有互相影响。
(因为在 Child 使用了 React.memo
, 所以 otherValue
的改变不会导致 Child 组件重新渲染。如果不使用 React.memo,点击 Change Other Value
按钮时,Child 组件会重新渲染)
但是,如果 count
和 increment
在 Parent 组件中定义,那么每次 Parent 组件重新渲染时,都会创建新的 count
和 increment
函数,导致 Child 组件也重新渲染。
import React, { useState } from 'react';
// 将 Child 组件定义为函数组件并使用 React.memo
const Child = React.memo(({ count, increment }) => {
console.log('Child rendered');
return (
<div style={{ border: '1px solid black', width: '300px', padding: '10px' }}>
<h1>Child Count: {count}</h1>
<button onClick={increment}>Increment Count</button>
</div>
);
});
// Parent 组件: App
const App = () => {
const [count, setCount] = useState(0);
const [otherValue, setOtherValue] = useState('Hello');
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
const changeOtherValue = () => {
setOtherValue((prevValue) => (prevValue === 'Hello' ? 'World' : 'Hello'));
};
console.log('Parent rendered');
return (
<div>
<h1>otherValue: {otherValue}</h1>
<button onClick={changeOtherValue}>Change Other Value</button>
<Child count={count} increment={increment} />
</div>
);
};
export default App;
点击查看输出
可以看到,otherValue
变化时,这个输出不太合理, Child 组件没有使用 otherValue
但也重新渲染了。
这是因为每次 Parent 组件重新渲染时,都会创建新的 increment 函数。对于 Child 组件来说传入的 increment
导致 props
不同,所以也会重新渲染。
此时,就可以使用 useCallback
来缓存 increment
函数,避免每次都重新创建。
useCallback 的语法:
const memoizedCallback = useCallback(fn, dependencies);
// fn:回调函数
// dependencies:依赖数组。当依赖数组中的值发生变化时,才会重新生成回调函数
使用 useCallback
把 Parent 组件传入的 increment
函数缓存起来:
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
// 示例的函数比较简单,并不需要响应任何状态或属性的变化,只需要在组件首次渲染时创建就可以了,所以依赖数组为空数组。
看一下效果:
可以看到,otherValue
变化时,Child 组件没有重新渲染,达到了我们想要的效果。
在实际应用中,React.memo
和 useCallback
经常结合使用,以减少不必要的组件渲染和函数创建,从而提高性能。
useMemo
说到这里,不得不提 React 提供的另一个 Hook: useMemo
。 其用于缓存计算结果,避免在每次渲染时都重新计算。
文档:https://zh-hans.react.dev/reference/react/useMemo
useMemo 的语法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// computeExpensiveValue:计算函数
// [a, b]:依赖数组。当依赖数组中的值发生变化时,才会重新计算
使用场景
某些时候,组件中某些值需要根据状态进行一个二次计算(类似于 Vue 中的计算属性),由于组件一旦重新渲染,就会重新执行整个函数,这就导致之前的二次计算也会重新执行一次,从而浪费性能。
例如,我们实现一个购物车时,总价需要根据当前购物车里面的商品内容进行计算,如果每次组件重新渲染时都重新计算总价,就会浪费性能。这时,我们就可以使用 useMemo
来缓存计算结果,避免每次都重新计算。
示例
还是是上面的例子,我们现在要根据 count
的值来计算一个num
。
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const [otherValue, setOtherValue] = useState('Hello');
console.log('App 渲染了');
function getNum() {
console.log('getNum调用了');
return count + 100;
}
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
const changeOtherValue = () => {
setOtherValue((prevValue) => (prevValue === 'Hello' ? 'World' : 'Hello'));
};
return (
<div>
<h1>getNum:{getNum()}</h1>
<h1>otherValue: {otherValue}</h1>
<div>
<button onClick={increment}>Increment Count</button>
<button onClick={changeOtherValue}>Change Other Value</button>
</div>
</div>
);
}
export default App;
运行一下,点击按钮,可以看到控制台输出:
可以看到,不管是更新 count
还是 otherValue
,getNum
都会重新调用。但是,当 otherValue
变化时,其实没必要重新执行 getNum
。
此时就可以使用 useMemo
来缓存 getNum
的计算结果:
import React, { useState, useMemo } from 'react';
function App() {
const [count, setCount] = useState(0);
const [otherValue, setOtherValue] = useState('Hello');
console.log('App 渲染了');
const getNum = useMemo(() => {
console.log('getNum调用了');
return count + 100;
}, [count]);
// 依赖数组为[count],只有当 count 变化时,才会重新计算 getNum
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
const changeOtherValue = () => {
setOtherValue((prevValue) => (prevValue === 'Hello' ? 'World' : 'Hello'));
};
return (
<div>
<h1>getNum:{getNum}</h1>
<h1>otherValue: {otherValue}</h1>
<div>
<button onClick={increment}>Increment Count</button>
<button onClick={changeOtherValue}>Change Other Value</button>
</div>
</div>
);
}
export default App;
运行,点击按钮,可以看到控制台输出:
可以看到,当 otherValue
变化时,getNum
没有重新调用,达到了我们想要的效果。
总结
下面对 React.memo
、useCallback
和 useMemo
进行一个简单的对比总结:
特性 | React.memo | useCallback | useMemo |
---|---|---|---|
主要功能 | 缓存组件,防止不必要的渲染 | 缓存回调函数 | 缓存计算结果 |
使用场景 | 当传入的 props 没有变化时,避免组件重新渲染 | 传递函数到子组件时,避免重新渲染时重新创建该函数 | 避免在每次渲染时,进行不必要的昂贵计算 |
依赖项 | 根据 props 变化 | 根据依赖数组变化 | 根据依赖数组变化 |
返回值类型 | 返回新的组件 | 返回记忆化的函数 | 返回记忆化的值 |