React之组件渲染性能优化

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

shouldComponentUpdate 与 PureComnent

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

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

先看一个类组件示例

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 代码如下:

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 组件的示例:

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

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 组件中:

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 组件也重新渲染。

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.memouseCallback 经常结合使用,以减少不必要的组件渲染和函数创建,从而提高性能。

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 还是 otherValuegetNum 都会重新调用。但是,当 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.memouseCallbackuseMemo 进行一个简单的对比总结:

特性React.memouseCallbackuseMemo
主要功能缓存组件,防止不必要的渲染缓存回调函数缓存计算结果
使用场景当传入的 props 没有变化时,避免组件重新渲染传递函数到子组件时,避免重新渲染时重新创建该函数避免在每次渲染时,进行不必要的昂贵计算
依赖项根据 props 变化根据依赖数组变化根据依赖数组变化
返回值类型返回新的组件返回记忆化的函数返回记忆化的值

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/895863.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

10 排序算法:冒泡排序与快速排序(算法原理、算法实现、时间和空间复杂度分析)

目录 1 十大常见的排序算法 1.1 算法的稳定性 2 冒泡排序 2.1 算法原理 2.2 算法实现 2.3 时间空间复杂度分析 2.3.1 时间复杂度分析 2.3.2 空间复杂度分析 3 快速排序 3.1 算法原理 3.1.1 排序思想 3.1.2 递归过程 3.2 示例 3.2.1 示例 1 3.2.2 示例 2 3.2.3 …

RHCE--网络服务

第一章 例行性工作 1、单一执行的例行性工作&#xff08;at&#xff09; 1.1 查看at命令 at的黑名单&#xff08;deny&#xff09;、白名单&#xff08;allow&#xff09;&#xff1b;两个文件若都不存在则只有root用户能使用 at工作调度对应的系统服务 atd&#xff1a;at的…

N9305高品质mp3音频语音芯片ic在早教故事机的应用方案

随着人们对教育的重视程度不断提高&#xff0c;儿童早教机已经成为了很多家庭的教育必备品。N9305音乐芯片在早教故事机中的应用&#xff0c;不仅为孩子们带来了丰富多彩的故事世界&#xff0c;还以其卓越的音质表现和功能&#xff0c;进一步提升了早教体验。 九芯电子N9305高品…

单片机——ADC采样

1、什么是ADC采样&#xff1f; ADC是指将模拟信号转换成数字信号的过程。通俗理解ADC采样就是采集电路中的电压&#xff0c;通过数值的方式表现出来。以STM32F103系列为例&#xff0c;它可以反应0~4095&#xff0c;换句话说&#xff0c;它采集的电压数值上表现为0~4095&#xf…

前端文件流导出

1、前端代码 ​ /** 导出 */ const handleExport async () > {let config {responseType: blob,headers: {Content-Type: application/json,},};const res await getTargetExport(config);const blob new Blob([res]);const fileName PK目标跟进导出列表.xls;const li…

WEB前端作业1

<!DOCTYPE html> <html><head><meta charset"utf-8"><title>用户注册页面</title></head><style type"text/css">#center{text-align: center;background-color: #e9e9e9;}tr td,th{border:1px solid whi…

linux线程 | 同步与互斥 | 互斥(下)

前言&#xff1a;本篇文章主要讲述linux线程的互斥的知识。 讲解流程为先讲解锁的工作原理&#xff0c; 再自己封装一下锁并且使用一下。 做完这些就要输出一堆理论性的东西&#xff0c; 但博主会总结两条结论&#xff01;&#xff01;最后就是讲一下死锁。 那么&#xff0c; 废…

Java-多线程2

什么是线程&#xff1f; 线程是 cpu调度和执行的单位。 多个线程共享进程的堆和方法区资源&#xff0c;但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。 如何实现线程 继承Thread类 实现步骤&#xff1a; 创建自定义类&#xff0c;继承Thread类 重写run方法 创建自定…

深度学习面试笔试之循环神经网络(RNN)、门控循环单元(GRU)、长短期记忆(LSTM)

深度学习面试笔试之循环神经网络RNN、门控循环单元GRU、长短期记忆LSTM 循环神经网络(RNN)1. 什么是RNN1.1 RNN的应用1.2 为什么有了CNN&#xff0c;还要RNN?1.3 RNN的网络结构1.4 双向RNN1.5 BPTT算法 2. 其它类型的RNN3. CNN与RNN的区别4. 为什么RNN 训练的时候Loss波动很大…

aws(学习笔记第七课) 私有子网使用NAT服务器

aws(学习笔记第七课) AWS的私有子网使用NAT服务器 学习内容&#xff1a; AWS的私有子网使用NAT服务器 1. AWS的私有子网使用NAT服务器 在上面的例子的网络构成图中&#xff0c;可能会发现一个问题。就是Private Subnet的Apache server无法访问互联网。比如&#xff0c;当需要…

MySQL【知识改变命运】10

联合查询 0.前言1.联合查询在MySQL里面的原理2.练习一个完整的联合查询2.1.构造练习案例数据2.2 案例&#xff1a;⼀个完整的联合查询的过程2.2.1. 确定参与查询的表&#xff0c;学⽣表和班级表2.2.2. 确定连接条件&#xff0c;student表中的class_id与class表中id列的值相等2.…

Win11右键默认显示更多选项

Win11默认显示 想要效果 解决方案1 先按住Shift键&#xff0c;再按右键试试。 解决方案2 1.启动命令行&#xff0c;输入命令 reg.exe add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve2.显示操作成功完成&#…

2024java高频面试之JVM-第二弹

什么是 STW Java 中「「Stop-The-World机制简称 STW」」 &#xff0c;是在执行垃圾收集算法时&#xff0c;Java 应用程序的其他所有线程都被挂起&#xff08;除了垃圾收集帮助器之外&#xff09;。「Java 中一种全局暂停现象&#xff0c;全局停顿」&#xff0c;所有 Java 代码…

子比美化 – WP添加网站翻译功能 | 实现国际化多语言[js翻译]

前言 本教程适用于子比主题&#xff0c;其它程序或主题请自行适配&#xff01;&#xff01;&#xff01; 图片展示 目前支持五种语言 教程开始 首先在后台自定义CSS代码中添加以下代码 .ignore:hover{color:var(--theme-color);transition:color .2s,transform .3s;}#tran…

怎么通过docker搭建一个mqtt服务器

由于debug需要排查mqtt的连接问题&#xff0c;为了方便&#xff0c;自己在云服务器上搭建一个mqtt服务器。 文中涉及的IP是虚构的IP&#xff0c;请替换成自己云服务器的IP&#xff0c;如有雷同&#xff0c;纯属巧合。 大致分为三部分&#xff1a; 一、安装docker 二、安装m…

cisco网络安全技术第3章测试及考试

测试 使用本地数据库保护设备访问&#xff08;通过使用 AAA 中央服务器来解决&#xff09;有什么缺点&#xff1f; 试题 1选择一项&#xff1a; 必须在每个设备上本地配置用户帐户&#xff0c;是一种不可扩展的身份验证解决方案。 请参见图示。AAA 状态消息的哪一部分可帮助…

<Project-11 Calculator> 计算器 0.2 工时计算器 WorkHours Calculator HTTP + JS

灵感 给工人发工资是按小时计算的&#xff0c;每次都要上网&#xff0c;我比较喜欢用 Hours Calculator &#xff0c;也喜欢它的其它的功能&#xff0c; 做个类似的。 我以为是 Python&#xff0c;结果在学 javascript 看 HTML&#xff0c;页面的基础还停留在 Frontpage 2000…

Cloudlog delete_oqrs_line 未授权SQL注入漏洞复现

0x01 产品简介 Cloudlog 是一个自托管的 PHP 应用程序,可让您在任何地方记录您的业余无线电联系人。使用PHP和MySQL构建的基于Web的业余无线电记录应用程序支持从HF到微波的一般站记录任务 0x02 漏洞概述 Cloudlog delete_oqrs_line 接口存在未授权SQL注入漏洞,未经身份验…

Marin说PCB之GMSL2 的Layout走线的注意事项

昨天有一位铁粉私信问我能不能讲解一下GMSL走线的一些注意事项啥的&#xff0c;我说当等我从以色列出差回来就给你更新一下这个&#xff0c;当然后来又很多的热心的粉丝提出很多的想法&#xff0c;我会一一给大家解答分享的&#xff0c;本期文章主要先给大家分享一下美信的手册…

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json 简介 pickle 模块 json 模块 pickle VS json 简介 什么叫序列化&#xff1f; 序列化指的是将对象转换为可以在网络上传输或者存储到文件系统中的字节流的过程。序列化使得对象可以被保存、传输和恢复&#…