React之组件渲染性能优化

news2024/10/17 9:54:27

关键词: 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 变化根据依赖数组变化根据依赖数组变化
返回值类型返回新的组件返回记忆化的函数返回记忆化的值

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

jar 导入本地和远程私服 maven 仓库

jar 导入本地和远程私服 maven 仓库artemis-http-client 认证库 maven 坐标为&#xff1a; 执行 mvn 命令&#xff1a; mvn install:install-file -DfileD:\download\lib\artemis-http-client-1.1.12.RELEASE.jar -DgroupIdcom.hikvision.ga -DartifactIdartemis-http-clien…

图像中的融合

图像显示函数 def img_show(name, img):"""显示图片:param name: 窗口名字:param img: 图片对象:return: None"""cv2.imshow(name, img)cv2.waitKey(0)cv2.destroyAllWindows()图像读取与处理 读取图片 cloud cv2.imread(bg.jpg) fish cv2.…

C++ | Leetcode C++题解之第485题最大连续1的个数

题目&#xff1a; 题解&#xff1a; class Solution { public:int findMaxConsecutiveOnes(vector<int>& nums) {int maxCount 0, count 0;int n nums.size();for (int i 0; i < n; i) {if (nums[i] 1) {count;} else {maxCount max(maxCount, count);coun…

【二刷hot-100】day1

目录 1.两数之和 2.字母异位词分组 3.字母异位词分组 4.最长连续序列 5.移动零 6.盛最多水的容器 7.三数之和 8.接雨水 1.两数之和 class Solution {public int[] twoSum(int[] nums, int target) {Map<Integer,Integer> mapnew HashMap<>();for (int i0;i<…

10.16标准IO

1、完成标准IO的单字符、字符串、格式化、模块化实现两个文件的拷贝&#xff1b; 单字符实现 #include <myhead.h> int main(int argc, const char *argv[]) {//使用单字符完成两个文件拷贝FILE *fp_srcfopen(argv[1],"r");FILE *fp_destfopen(argv[2],"…

猪圈密码简单实例

猪圈密码简单实例 猪圈密码表 根据上面的密码表&#xff0c;得到每个字母所对应的符号如下 例如单词the的加密结果为&#xff1a;

IO编程--单字符、字符串、格式化、模块化实现文件拷贝以及登录注册

一、完成标准io的单字符、字符串、格式化、模块化实现两个文件的拷贝 代码如下&#xff1a; 1.单字符 #include <myhead.h> int main(int argc, const char *argv[]) {//打开文件FILE* fpfopen("test.txt","r"); FILE* fqfopen("copy_test.txt&…

leetcode:744. 寻找比目标字母大的最小字母(python3解法)

难度&#xff1a;简单 给你一个字符数组 letters&#xff0c;该数组按非递减顺序排序&#xff0c;以及一个字符 target。letters 里至少有两个不同的字符。 返回 letters 中大于 target 的最小的字符。如果不存在这样的字符&#xff0c;则返回 letters 的第一个字符。 示例 1&a…

2024国际潜水小姐大赛中国区总决赛盛典在广州举行,吉林选手张潇文获冠军!

传承“以美之名&#xff0c;保护海洋”的精神&#xff0c;2024年10月15日晚&#xff0c;2024国际潜水小姐大赛中国区总决赛盛典在广州渔民新村隆重举行&#xff01;来自全国多个城市&#xff0c;经过层层选拔产生的20位佳丽齐聚广州&#xff0c;以独特的女性水下之美和健康美&a…

初识算法 · 二分查找(1)

目录 前言&#xff1a; 二分查找 题目解析 算法原理 算法编写 搜索插入位置 题目解析 算法原理 算法编写 前言&#xff1a; 本文呢&#xff0c;我们从滑动窗口窗口算法移步到了二分查找算法&#xff0c;我们简单了解一下二分查找算法&#xff0c;二分查找算法是一个十…

安科瑞末端组合式智慧用电装置在高校宿舍中的应用

1相关背景 学校宿舍用电隐患 安全用电历来都是学校安全工作的一个重点&#xff0c;然而每年因此发生的人身伤害以及火灾事故却在继续&#xff0c;究其原因&#xff0c;主观上是我们的防患意识淡薄&#xff0c;客观上则是由于学生在宿舍使用违规电器、乱拉电线造成的。 现代的…

Java IO 基础知识

IO 流简介 IO 即 Input/Output&#xff0c;输入和输出。数据输入到计算机内存的过程即输入&#xff0c;反之输出到外部存储&#xff08;比如数据库&#xff0c;文件&#xff0c;远程主机&#xff09;的过程即输出。数据传输过程类似于水流&#xff0c;因此称为 IO 流。IO 流在…

java关于如何实现读取各种类型的文件核心属性方法,比如获取标题和作者、主题等;附带远程的https的地址文件读取方法;

有两种方法&#xff1a; 通过提供的现成api进行调用读取pdf文件&#xff0c;或doc、xlsx、pptx文件&#xff1b;可能商业需要付费 https://www.e-iceblue.cn/pdf_java_document_operation/set-pdf-document-properties-in-java.html Spire.PDF for Java import com.spire.pdf…

为什么SSH协议是安全的?

SSH的传输层协议&#xff08;Transport Layer Protocol&#xff09;和用户鉴权协议&#xff08;Authentication Protocol&#xff09;确保数据的传输安全&#xff0c;这里只介绍传输层协议&#xff0c;是SSH协议的基础。 本文针对SSH2协议。 1、客户端连接服务器 服务器默认…

基于springboot实习管理系统

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 系统展示 【2024最新】基于JavaSpringBootVueMySQL的&#xff0c;前后端分离。 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;…

k8s use-context是什么

kubectl 的 use-context 命令用于在 Kubernetes 集群中切换上下文&#xff08;context&#xff09;&#xff0c;从而方便地在多个集群或命名空间之间进行操作。一个上下文定义了 kubectl 使用的 集群、用户 和 命名空间 的组合。 use-context 的作用&#xff1a; 每个上下文&…

msql事务隔离级别 线上问题

1. 对应代码 解决方式&#xff1a; 在事务隔离级别为可重复读&#xff08;RR&#xff09;时&#xff0c;数据库确实通常会记录当前数据的快照。 在可重复读隔离级别下&#xff0c;事务在执行期间看到的数据是事务开始时的数据快照&#xff0c;即使其他事务对数据进行了修改&am…

考研前所学c语言02(2024/10/16)

1.一个十进制的数转化为二进制的就是不断除二取余&#xff0c;得到的余数从下到上取 比如123&#xff1a; 结果为&#xff1a; 同理其他的十进制转八进制&#xff0c;十六进制就除八&#xff0c;除十六即可 再比如123转十六进制&#xff1a; 因为余数是11&#xff0c;十六进…

超详细的finalshell安装数据库以及数据库的基本操作

一、下载 MySQL Enterprise Edition Downloads | Oraclehttps://www.oracle.com/mysql/technologies/mysql-enterprise-edition-downloads.html 这边有不同的版本&#xff0c;要看你的操作系统&#xff08;centos7 / centos8&#xff09;安装的是哪个版本 我把连接提取出来了&…

Vivado - Aurora 8B/10B IP

目录 1. 简介 2. 设计调试 2.1 Physical Layer 2.2 Link Layer 2.3 Receiver 2.4 IP 接口 2.5 调试过程 2.5.1 Block Design 2.5.2 释放 gt_reset 2.5.3 观察数据 3. 实用技巧 3.1 GT 坐标与布局 3.1.1 选择器件并进行RTL分析 3.1.2 进入平面设计 3.1.3 收发器布…