React之组件渲染性能优化

关键词: shouldComponentUpdate、PureComnent、React.memo、useMemo、useCallback

shouldComponentUpdate 与 PureComnent

shouldComponentUpdatePureComnent 用于类组件。虽然官方推荐使用函数组件,但我们依然需要对类组件的渲染优化策略有所了解,不仅是维护旧的类组件代码需要,很多优化的概念是通用的。

所以,我们先简单了解一下 shouldComponentUpdatePureComnent

先看一个类组件示例

js 复制代码
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)的 stateprops 发生变化时,React 会默认重新渲染该组件及其所有 Child 组件。

此时就可以用到shouldComponentUpdate 来优化性能,避免不必要的渲染。

shouldComponentUpdate

文档:https://zh-hans.react.dev/reference/react/Component#shouldcomponentupdate

shouldComponentUpdate 是一个生命周期方法,可以用来决定组件是否需要更新。返回 true 会让组件继续更新,而返回 false 则会阻止更新。

使用 shouldComponentUpdate 优化后的 Child 代码如下:

js 复制代码
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 ValueotherValue 会改变,但 Child 组件不会重新渲染:

PureComponent

除了手动实现 shouldComponentUpdate,我们还可以使用 React.PureComponent来自动处理这一逻辑。PureComponent 会对其 props 进行浅比较,如果 props 没有变化,则不会重新渲染。

下面是使用 PureComponent 重写 Counter 组件的示例:

js 复制代码
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

js 复制代码
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 还可以接受第二个参数,用于自定义比较逻辑。第二个参数是一个函数,接受两个参数:oldPropsnewProps,返回一个布尔值,表示是否需要重新渲染组件。

js 复制代码
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 组件。我们还是以之前的例子为例,把 countincrement 放到 Child 组件中:

js 复制代码
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 组件会重新渲染)

但是,如果 countincrement 在 Parent 组件中定义,那么每次 Parent 组件重新渲染时,都会创建新的 countincrement 函数,导致 Child 组件也重新渲染。

js 复制代码
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 的语法:
js 复制代码
const memoizedCallback = useCallback(fn, dependencies);
// fn:回调函数
// dependencies:依赖数组。当依赖数组中的值发生变化时,才会重新生成回调函数

使用 useCallback 把 Parent 组件传入的 increment 函数缓存起来:

js 复制代码
const increment = useCallback(() => {
	setCount((prevCount) => prevCount + 1);
}, []);
// 示例的函数比较简单,并不需要响应任何状态或属性的变化,只需要在组件首次渲染时创建就可以了,所以依赖数组为空数组。

看一下效果:

可以看到,otherValue 变化时,Child 组件没有重新渲染,达到了我们想要的效果。

在实际应用中,React.memouseCallback 经常结合使用,以减少不必要的组件渲染和函数创建,从而提高性能。

useMemo

说到这里,不得不提 React 提供的另一个 Hook: useMemo。 其用于缓存计算结果,避免在每次渲染时都重新计算。

文档:https://zh-hans.react.dev/reference/react/useMemo

useMemo 的语法:
js 复制代码
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// computeExpensiveValue:计算函数
// [a, b]:依赖数组。当依赖数组中的值发生变化时,才会重新计算
使用场景

某些时候,组件中某些值需要根据状态进行一个二次计算(类似于 Vue 中的计算属性),由于组件一旦重新渲染,就会重新执行整个函数,这就导致之前的二次计算也会重新执行一次,从而浪费性能。

例如,我们实现一个购物车时,总价需要根据当前购物车里面的商品内容进行计算,如果每次组件重新渲染时都重新计算总价,就会浪费性能。这时,我们就可以使用 useMemo 来缓存计算结果,避免每次都重新计算。

示例

还是是上面的例子,我们现在要根据 count 的值来计算一个num

js 复制代码
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 还是 otherValuegetNum 都会重新调用。但是,当 otherValue 变化时,其实没必要重新执行 getNum

此时就可以使用 useMemo 来缓存 getNum 的计算结果:

js 复制代码
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.memouseCallbackuseMemo 进行一个简单的对比总结:

特性 React.memo useCallback useMemo
主要功能 缓存组件,防止不必要的渲染 缓存回调函数 缓存计算结果
使用场景 当传入的 props 没有变化时,避免组件重新渲染 传递函数到子组件时,避免重新渲染时重新创建该函数 避免在每次渲染时,进行不必要的昂贵计算
依赖项 根据 props 变化 根据依赖数组变化 根据依赖数组变化
返回值类型 返回新的组件 返回记忆化的函数 返回记忆化的值
相关推荐
264玫瑰资源库10 分钟前
问道数码兽 怀旧剧情回合手游源码搭建教程(反查重优化版)
java·开发语言·前端·游戏
喝拿铁写前端21 分钟前
从圣经Babel到现代编译器:没开玩笑,普通程序员也能写出自己的编译器!
前端·架构·前端框架
HED27 分钟前
VUE项目发版后用户访问的仍然是旧页面?原因和解决方案都在这啦!
前端·vue.js
拉不动的猪1 小时前
前端自做埋点,我们应该要注意的几个问题
前端·javascript·面试
王景程1 小时前
如何测试短信接口
java·服务器·前端
安冬的码畜日常1 小时前
【AI 加持下的 Python 编程实战 2_10】DIY 拓展:从扫雷小游戏开发再探问题分解与 AI 代码调试能力(中)
开发语言·前端·人工智能·ai·扫雷游戏·ai辅助编程·辅助编程
烛阴1 小时前
Node.js中必备的中间件大全:提升性能、安全与开发效率的秘密武器
javascript·后端·express
小杨升级打怪中1 小时前
前端面经-JS篇(三)--事件、性能优化、防抖与节流
前端·javascript·xss
清风细雨_林木木2 小时前
Vue开发网站会有“#”原因是前端路由使用了 Hash 模式
前端·vue.js·哈希算法
鸿蒙布道师2 小时前
OpenAI为何觊觎Chrome?AI时代浏览器争夺战背后的深层逻辑
前端·人工智能·chrome·深度学习·opencv·自然语言处理·chatgpt