为什么会存在重复渲染?

react 在 v16.8 版本引入了全新的 api,叫做 React Hooks,它的使用与以往基于 class component 的组件用法非常的不一样,不再是基于类,而是基于函数进行页面的渲染,我们把它又称为 functional component

因为 react hook 使用的是函数组件,父组件的任何一次修改,都会导致子组件的函数执行,从而重新进行渲染。

那么下面我们考虑三种情况:

  • 父组件没有 props 传入子组件 props
  • 父组件传入子组件的 props 都是简单数据类型
  • 父组件传入子组件的 props 存在复杂数据类型

React.memo 为高阶组件。它与 React.PureComponent 非常相似。默认只会对复杂类型对象做浅层比较,如果需要控制对比过程我们可以将比较函数作为第二个参数传入:React.memo(MyComp, areEqual)

父组件没有 props 传入子组件 props

在这种情况下,子组件的渲染不需要依赖父组件值的变化,使用 React.memo 包裹子组件,即缓存下子组件。这样,父组件中的数值如何变化,都会使用缓存下来的子组件。

父组件传入子组件的 props 都是简单数据类型

在这种情况下,父组件传入子组件的 props 都是简单数据类型,浅层对比即可判断是否发生了变化,使用 React.memo 包裹子组件,也可以解决重复渲染的问题。

父组件传入子组件的 props 存在复杂数据类型

父组件通过 props 向子组件传值时,可能需要传入复杂类型如 object,以及 function 类型的值。而 memo 子组件进行渲染比对时进行的是浅比较,即使我们传入相同的 objectfunction,子组件也会认为传入参数存在修改,从而子组件重新进行渲染。这个时候仅仅使用 memo 包裹子组件应该没办法解决问题了,是时候用上我们的 useCallback 以及 useMemo 了。

下面我们来看一下 React.memo 的使用。

React.memo 的使用

例如,一个父组件 Home 中渲染了子组件 List,同时 Home 组件还有一个计数器组件,每次点击 count 都会加 1,遇到类似的场景就会出现子组件重复渲染问题,这是因为 React 中当父组件的一个状态改变后,无论和子组件是否有关,子组件都会受到影响进行重新渲染,这也是 React 中默认的一个行为。

函数组件中的解决方案是使用 React.memo() 函数,将需要优化的函数组件传入即可。

import React, { useEffect, useState } from "react";

// 未使用 memo:const List = ({ dataList }) => {
const List = React.memo(({ dataList }) => {
console.log("List 渲染"); return (
<div>
{dataList.map((item) => (
<h2 key={item.id}> {item.title} </h2>
))}
</div>
);
}); const Home = () => {
const [count, setCount] = useState(0);
const [dataList, setDataList] = useState([]); useEffect(() => {
const list = [
{ title: "React 性能优化", id: 1 },
{ title: "Node.js 性能优化", id: 2 },
];
setDataList(list);
}, []); return (
<div>
<button type="button" onClick={() => setCount(count + 1)}>
count: {count}
</button>
<List dataList={dataList} />
</div>
);
}; export default Home;

自定义控制对比过程

函数 React.memo() 还提供了第二个参数 propsAreEqual,用来自定义控制对比过程。

// React.memo() 的 TypeScript 类型描述
function memo<T extends ComponentType<any>>(
Component: T,
propsAreEqual?: (
prevProps: Readonly<ComponentProps<T>>,
nextProps: Readonly<ComponentProps<T>>
) => boolean
): MemoExoticComponent<T>;

使用memo, useMemo, useCallback

useCallback

先来说下,经常使用一些的 useCallback

// 仅使用了memo,父组件传递给子组件的prop为方法,
// 该方法在子组件中被调用,改变了父组件的值,导致父组件重新渲染。
// 又由于父组件重新渲染,传给子组件的方法因其引用地址的不同会被认为有修改,导致子组件出现了不必要的重新渲染。
const Child = memo((props) => {
console.log('我是一个子组件');
return (
<button onClick={props.handleClick}>改变父组件中的年龄</button>
)
}) const Father = () => {
console.log('我是一个父组件')
const [age, setAge] = useState(0);
return (
<div>
<span>`目前的count值为${age}`<span>
<Child handleClick={() => setAge(age + 1)}/>
</div>
)
}
// 使用了useCallback优化了传递给子组件的函数,只初始化一次这个函数,下次不产生新的函数
const Father = () => {
console.log('我是一个父组件')
const [age, setAge] = useState(0);
return (
<div>
<span>`目前的年龄为${age}`<span>
<Child handleClick={useCallback(() => setAge(age + 1), [])}/>
</div>
)
}

注意:在 useCallback 的第二个参数处要传入正确的依赖值,否则 useCallback 就不会重新执行,其中使用的变量就还是之前的值,useMemo也是如此。

我们在方法中可能会使用一些组件中但是存在方法外的参数,我们一定要将这些参数放入依赖项中,否则会一直使用缓存的方法,里面的外部参数也会一直是旧值。

useMemo

// 使用了memo以及useCallback,我们会发现更新属性profile为对象时,
// 尽管子组件只改变了age的值且子组件并没有使用age字段,子组件还是执行了。
// 这是因为在父组件更新其他状态的情况下,子组件的profile作为复杂类型,
// 仅仅进行浅比较会被认为存在修改,从而会一直重新渲染改变,导致子组件函数一直执行,这也是不必要的性能浪费。
// 解决这个问题,就需要在profile属性上使用useMemo了
const Child = memo((props) => {
console.log('我是一个子组件');
const {profile, handleClick} = props;
return (
<div>
<div>{`父组件传来的用户信息:姓名${profile.name}, 性别${profile.gender}`}</div>
<button onClick={handleClick}>改变父组件age</button>
</div>
)
}) const Father = () => {
console.log('我是一个父组件')
const [age, setAge] = useState(0);
const [name, setName] = useState('张三男');
const
return (
<div>
<span>`目前的年龄为${age}`<span>
<Child
profile={{name, gender: name.indexOf('男') > -1 ? '男' : '女' }}
handleClick={useCallback(() => setAge(age + 1), [])}
/>
</div> )
}
// 使用useMemo,返回一个和原对象一样的对象,第二个参数是依赖性,仅当name发生改变的时候,才产生一个新的对象,注意:依赖项千万要填写正确,否则name改变时,profile依旧使用旧值,就会产生错误

const Father = () => {
console.log('我是一个父组件')
const [age, setAge] = useState(0);
const [name, setName] = useState('张三男');
const
return (
<div>
<span>`目前的年龄为${age}`<span>
<Child
profile={useMemo(() => ({
name,
gender: name.indexOf('男') > -1 ? '男' : '女' }), [name])
}
handleClick={useCallback(() => setAge(age + 1), [])}
/>
</div> )
}

React.memo 无效情况

第一种

React.memo 对普通的引用类型是无效的。例如,在 List 组件增加 user 属性,即使使用了 React.memo() ,每次点击 count, List 组件还会重复渲染。

const Home = () => {
const user = {name: '哈哈'};
... return (
<div>
<List dataList={dataList} user={user} />
</div>
);
};

React.memo() 结合使用时,普通引用类型对象需要通过 useMemouseState 处理,来避免组件的重复渲染。

const user = useMemo(() => ({ name: "哈哈" }), []);
const [user] = useState({ name: "哈哈" });

第二种

函数组中包括了一些 Hooks 例如 useStateuseContext,当上下文发生变化时,组件也同样会重新渲染,React.memo 在这里仅比较 props。上面例子中,如果把 button 组件放到 List 组件里,每次点击,List 也还是会被重新渲染。

const List = React.memo(({ dataList }) => {
console.log("List 渲染");
const [count, setCount] = useState(0); return (
<div>
<button type="button" onClick={() => setCount(count + 1)}>
List count: {count}
</button>
{dataList.map((item) => (
<h2 key={item.id}> {item.title} </h2>
))}
</div>
);
});

总结

React.memo() 是一个高阶组件,接收一个组件并返回一个新组件。它会记忆组件上次的 Props,同下次需要更新的 Props 做 “浅对比”,如果相同就不做更新,只有在不同时才会重新渲染。如果你的组件存在一些耗时的计算,每次重新渲染对页面性能显然是糟糕的,这时 React.memo() 对你来说也许是一个好的选择。并不是所有的组件都要引入 React.memo(),自身浅对比这个过程也会有一些消耗,如果没有特殊需求,也不一定非要使用。

  1. 子组件没有从父组件传入的 props 或者传入的 props 仅仅为简单数值类型使用 memo 即可
  2. 子组件有从父组件传来的方法时,在使用 memo 的同时,使用 useCallback 包裹该方法,传入方法需要更新的依赖值
  3. 子组件有从父组件传来的对象和数组等值时,在使用 memo 的同时,使用 useMemo 以方法形式返回该对象,传入需要更新的依赖值

React.memo 解决函数组件重复渲染的更多相关文章

  1. react高阶函数组件

    Layout as a Higher Order Component // components/MyLayout.js import Header from './Header'; const la ...

  2. React性能优化,六个小技巧教你减少组件无效渲染

    壹 ❀ 引 在过去的一段时间,我一直围绕项目中体验不好或者无效渲染较为严重的组件做性能优化,多少积累了一些经验所以想着整理成一片文章,下图就是优化后的一个组件,可以对比优化前一次切换与优化后多次切换的 ...

  3. react如何通过shouldComponentUpdate来减少重复渲染

    转自:https://segmentfault.com/a/1190000016494335 在react开发中,经常会遇到组件重复渲染的问题,父组件一个state的变化,就会导致以该组件的所有子组件 ...

  4. React.memo

    介绍React.memo之前,先了解一下React.Component和React.PureComponent. React.Component React.Component是基于ES6 class ...

  5. 深入了解React组件重新渲染的条件和生命周期

    React组件rerender的真正条件 当前组件的State中的属性改变时且当前组件的shouldcomponentupdate返回true,那么当前组件会rerender 组件的props中的任一 ...

  6. react基础用法二(组件渲染)

    react基础用法二(组件渲染) 如图所示组件可以是函数 格式:function 方法名(){ return <标签>内容</标签>} 渲染格式: <方法名 />  ...

  7. React - 组件:函数组件

    目录: . 组件名字首字母一定是大写的 . 返回一个jsx . jsx依赖React,所以组件内部需要引入React . 组件传参 a. 传递. <Component list={ arrDat ...

  8. react hooks 如何自定义组件(react函数组件的封装)

    前言 这里写一下如何封装可复用组件.首先技术栈 react hooks + props-type + jsx封装纯函数组件.类组件和typeScript在这不做讨论,大家别白跑一趟. 接下来会说一下封 ...

  9. React 函数组件中对window添加事件监听resize导致回调不能获得Hooks最新状态的问题解决思路

    React 函数组件中对window添加事件监听resize导致回调不能获得Hooks最新状态的问题解决思路 这几天在忙着把自己做的项目中的类组件转化为功能相同的函数组件,首先先贴一份该组件类组件的关 ...

  10. React 函数组件

    React 函数组件 1.定义方式 React 函数组件是指使用函数方法定义的组件. 定义方式:与函数的定义方式相同,需要将内容 return 出来,需要注意的是最外层只有一个标签或者使用<&g ...

随机推荐

  1. JVM实战—1.Java代码的运行原理

    大纲 1.Java代码到底是如何运行起来的 2.JVM类加载机制的一系列概念 3.JVM中有哪些内存区域及各自的作用 4.JVM的垃圾回收机制的作用 5.问题汇总 1.Java代码到底是如何运行起来的 ...

  2. [转]Visual Studio调试模式下添加命令行参数的方法

    在VS中向命令行添加参数,即向main()函数传递参数的方法: 右键单击:添加参数的工程-->属性-->配置属性-->调试,在右侧"命令参数"栏输入要添加的参数, ...

  3. 阿里IM技术分享(九):深度揭密RocketMQ在钉钉IM系统中的应用实践

    本文由钉钉技术专家尹启绣分享,有修订和重新排版. 1.引言 短短的几年时间,钉钉便迅速成为一款国民级应用,发展速度堪称迅猛. IM作为钉钉最核心的功能,每天需要支持海量企业用户的沟通,同时还通过 Pa ...

  4. echo输出

    linux中不免经常使用echo进行输出,或输出到屏幕,或输出到文件.但是使用的时候会发现,在想要输出一些需要转义的字符时,例如\t等,它却原样不动的输出了. 使用man命令查看echo的帮助文档,会 ...

  5. Windows安全加固(四)

    七.服务安全 1.禁用TCP/IP上的NetBIOS(协议所用端口139) 作用:禁用TCP/IP上的NetBIOS协议,可以关闭监听的UDP137.UDP138.UDP139端口. (1)使用快捷键 ...

  6. Mac安装MySQL详细教程

    1.MySQL安装包下载 还没下载的话请前往官网下载 我们可以看到这里有两个不同架构的dmg的安装包,如果不知道自己电脑是ARM还是X86的话可以打开终端输入:uname -a 或者 uname -a ...

  7. w3cschool-Bootstrap 教程

    Bootstrap 简介 什么是 Bootstrap? Bootstrap 是一个用于快速开发 Web 应用程序和网站的前端框架.Bootstrap 是基于 HTML.CSS.JAVASCRIPT 的 ...

  8. C#/.NET/.NET Core技术前沿周刊 | 第 21 期(2025年1.6-1.12)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  9. Peewee:Python 简洁强大的 ORM 框架

    在 Python 的开发世界中,数据库操作是至关重要的一环. 今天介绍的 Peewee 作为一款简洁且功能强大的 ORM(对象关系映射)框架,为开发者提供了高效便捷的数据库交互方式. 1. Peewe ...

  10. System类、Math类、BigInteger与BigDecimal的使用

     System类代表系统,系统级的很多属性和控制方法都放置在该类的内部.该类位于java.lang包. 由于该类的构造器是private的,所以无法创建该类的对象,也就是无法实例化该类.其内部的成 ...