React.memo 解决函数组件重复渲染
为什么会存在重复渲染?
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 子组件进行渲染比对时进行的是浅比较,即使我们传入相同的 object 或 function,子组件也会认为传入参数存在修改,从而子组件重新进行渲染。这个时候仅仅使用 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() 结合使用时,普通引用类型对象需要通过 useMemo、useState 处理,来避免组件的重复渲染。
const user = useMemo(() => ({ name: "哈哈" }), []);
const [user] = useState({ name: "哈哈" });
第二种
函数组中包括了一些 Hooks 例如 useState、useContext,当上下文发生变化时,组件也同样会重新渲染,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(),自身浅对比这个过程也会有一些消耗,如果没有特殊需求,也不一定非要使用。
- 子组件没有从父组件传入的 props 或者传入的 props 仅仅为简单数值类型使用
memo即可 - 子组件有从父组件传来的方法时,在使用
memo的同时,使用useCallback包裹该方法,传入方法需要更新的依赖值 - 子组件有从父组件传来的对象和数组等值时,在使用
memo的同时,使用useMemo以方法形式返回该对象,传入需要更新的依赖值
React.memo 解决函数组件重复渲染的更多相关文章
- react高阶函数组件
Layout as a Higher Order Component // components/MyLayout.js import Header from './Header'; const la ...
- React性能优化,六个小技巧教你减少组件无效渲染
壹 ❀ 引 在过去的一段时间,我一直围绕项目中体验不好或者无效渲染较为严重的组件做性能优化,多少积累了一些经验所以想着整理成一片文章,下图就是优化后的一个组件,可以对比优化前一次切换与优化后多次切换的 ...
- react如何通过shouldComponentUpdate来减少重复渲染
转自:https://segmentfault.com/a/1190000016494335 在react开发中,经常会遇到组件重复渲染的问题,父组件一个state的变化,就会导致以该组件的所有子组件 ...
- React.memo
介绍React.memo之前,先了解一下React.Component和React.PureComponent. React.Component React.Component是基于ES6 class ...
- 深入了解React组件重新渲染的条件和生命周期
React组件rerender的真正条件 当前组件的State中的属性改变时且当前组件的shouldcomponentupdate返回true,那么当前组件会rerender 组件的props中的任一 ...
- react基础用法二(组件渲染)
react基础用法二(组件渲染) 如图所示组件可以是函数 格式:function 方法名(){ return <标签>内容</标签>} 渲染格式: <方法名 /> ...
- React - 组件:函数组件
目录: . 组件名字首字母一定是大写的 . 返回一个jsx . jsx依赖React,所以组件内部需要引入React . 组件传参 a. 传递. <Component list={ arrDat ...
- react hooks 如何自定义组件(react函数组件的封装)
前言 这里写一下如何封装可复用组件.首先技术栈 react hooks + props-type + jsx封装纯函数组件.类组件和typeScript在这不做讨论,大家别白跑一趟. 接下来会说一下封 ...
- React 函数组件中对window添加事件监听resize导致回调不能获得Hooks最新状态的问题解决思路
React 函数组件中对window添加事件监听resize导致回调不能获得Hooks最新状态的问题解决思路 这几天在忙着把自己做的项目中的类组件转化为功能相同的函数组件,首先先贴一份该组件类组件的关 ...
- React 函数组件
React 函数组件 1.定义方式 React 函数组件是指使用函数方法定义的组件. 定义方式:与函数的定义方式相同,需要将内容 return 出来,需要注意的是最外层只有一个标签或者使用<&g ...
随机推荐
- 『玩转Streamlit』--上传下载文件
在Web应用中,文件的上传下载是交互中不可缺少的功能. 因为在业务功能中,一般不会只有文字的交互,资料或图片的获取和分发是很常见的需求. 比如,文件上传可让用户向服务器提交数据,如上传图片分享生活.提 ...
- 查看MySQL数据库所有的表名、表注释、字段名称、类型、长度、备注,一键导出生成数据库字典
一.先了解下INFORMATION_SCHEMA1.在MySQL中,把INFORMATION_SCHEMA看作是一个数据库,确切说是信息数据库.其中保存着关于MySQL服务器所维护的所有其他数据库的信 ...
- Qt编写地图综合应用5-自适应拉伸
一.前言 用过echart的人都会遇到一个问题,就算是代码中写了window.onresize = echart.resize,也只是横向自适应拉伸填充页面,垂直方向不会变化,除非指定高度才可以,这就 ...
- [转]CopyPlugin Invalid Options options should be array ValidationError: CopyPlugin Invalid Options
这个错误是使用webpack的一个plugin出现的错误.这个plugin是copy-webpack-plugin我把文档的例子复制,然后就报了这个错误.文档的例子: const CopyPlugin ...
- asp.net mvc中换肤机制类库 ThemedViewEngines
制作blog系统或者通用cms系统的时候,我们经常会用到Theme功能.asp.net mvc中的一种实现方式,是继承实现RazorViewEngine即可. 这是在GitHub中找到的一个示例:ht ...
- 查看Android是否开机启动进入桌面
adb 或者 串口终端 getprop sys.boot_completed 返回空代表没有进入桌面返回1代表已进入桌面
- 工程目录下.git目录过大文件清理
1. 查看当前5个大文件 git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx ...
- document.querySelector 有多个类 的情况
document.querySelector 有多个类 document.querySelector 方法用于返回文档中匹配指定 CSS 选择器的第一个元素.如果要查询具有多个类的元素,可以将它们作为 ...
- JAVA常见问题合集
面向对象 面向过程和面向对象 面向对象的三大基本特征:封装.继承.多态 - 封装:隐藏内部细节 继承:复用现有代码 多态:改写对象行为 JAVA为什么是面向对象的,为什么还用int等基础类型 面向对象 ...
- struts2和Springmvc原理及比较
一.拦截机制的不同 Struts2是类级别的拦截,每次请求就会创建一个Action,和Spring整合时Struts2的ActionBean注入作用域是原型模式prototype,然后通过setter ...