背景
在React中,组件渲染的是最常有的事情。但是,有部分的渲染是不必要的,是可以避免的。
在react的一般规则中,只有父组件的某一个状态改变,父组件下面所有的子组件不论是否使用了该状态,都会进行重新渲染。
显然,对于没有用到被改变的那个状态的组件来说,重新渲染是完全没有必要的。所以,React.memo就诞生了。
什么是 React.memo
React.memo 是一个高阶组件,它可以用来包装一个函数组件(Functional Component)并返回一个新的组件。这个新组件会对传入的 props 进行浅比较(Shallow Comparison),如果发现传入的 props 没有发生变化,则直接返回上一次渲染的结果,从而避免不必要的重复渲染过程,提升了组件的渲染性能。否则重新渲染组件。
React.memo 和 React.PureComponent 类似,都可以用于优化组件的性能。但是它们之间也存在着很大的区别,React.PureComponent 只适用于 class 组件,而 React.memo 则适用于所有函数组件。
React特性:父组件中状态的改变会让所有的子组件重新渲染
比如:
import React, { useMemo, useState } from "react";export const EgOfMemo: React.FC = () => {// 传入子组件的状态const [name, setName] = useState('lvxiaobu')// 没有传入子组件的状态,但是当age更新后,父组件会重新渲染,所有的子组件无论是否用到age,也全部会重新渲染const [age, setAge] = useState(0)return (<div>{/* 点击按钮,子组件Child会重新渲染 */}<button onClick={() => setAge(1)}> 点击修改年龄</button><Child name={name} /></div>)
}
只要父组件的状态改变,所有的子组件不论是否使用到了被改变的那个state都会被重新渲染。
显然这种渲染是完全没必要的。我又没有使用被改变的那个state,我本身本身也没有什么视图需要更新,根本没必要重新渲染。
在阻止重新渲染这个需求的基础上,诞生了memo函数,memo是react的一种缓存技术,这个函数可以检测从父组件接收的props,并且在父组件改变state的时候比对这个state是否是本组件在使用,如果不是,则不会重新渲染。
用法
使用 React.memo 很简单,只需要将要优化的函数组件作为参数传递给 React.memo 即可,例如:
import React, { memo } from 'react';function Child(props) {// 子组件的具体实现逻辑
}export default memo(Child);
在上面的例子中,我们使用 memo 包装了一个名为Child的函数组件,并导出了一个新的组件。此时,这个新组件会对传入的 props 进行浅比较,以便在 props 没有发生变化时直接使用上一次渲染的结果。
像这样,这Child就是被缓存成功了。下次当父组件中无关它的state(状态)被更新时候,Child组件就不会重新渲染了。
总之就是,如果当前组件被memo保护,那么当前组件的props不变,则组件不进行重新渲染。这样,我们合理的使用memo就可以为我们的项目带来很大的性能优化。
注意事项
虽然 React.memo 可以帮助我们优化组件的性能和渲染速度,但也存在着一些需要注意的事项。
1、首先,由于 React.memo 只进行浅比较,因此如果 props 中包含了引用类型的数据(如数组或对象),那么即使这些引用类型的数据没有发生变化,也会触发重新渲染。因此,需要特别注意在 props 中传递引用类型的数据时,要确保它们不会频繁地发生变化,才能获得最佳的性能表现。
import React, { useState } from "react";
import { EgOfMemo } from '../../components'export const Home: React.FC = () => {const [name, setName] = useState('');const [list, setList] = useState([1,2,3])const modifyName = () => {setName('lvxiaobu')list.push(4)// list更新了,但子组件没有重新渲染console.log(1, list)}return (<><div>{name}</div><button onClick={modifyName}>点击</button><EgOfMemo list={list} /></>)
}
上面传入了一个list数组进去子组件,子组件内部是被memo缓存了的。点击按钮的时候,如果我们往list这个数组中push一个4,那么子组件中的props改变了,理论上来说,子组件应该重新渲染了。但实际上并不会。
这是为什么呢?因为memo的保护是对props做一个浅比较(只比较栈,不比较堆)
而数组的使用push()方法看似是变了。但变的只是堆中的数据,存在与栈中的地址依然不会改变。memo是检测不到的。所以,使用push等不能返回一个新数组的方法,均无法触发memo的更新机制。
想要改变数组也能让子组件更新,有方法。
就像刚刚说的,需要让memo检测到数组栈地址的变化。要栈地址变化的话,只要返回一个全新的数组就好了,比如:
setList([...list, 4]); //这样才可以,创建一个新数组,再在里面解构旧数组,往后面追加 1
这样,就可以既修改数组,又触发组件更新了。
2、一旦传入给子组件的是函数类型,那么子组件使用memo包裹也没法让它避免没有意义的渲染
import React, { useState } from "react";
import { EgOfMemo } from '../../components'export const Home: React.FC = () => {let [name, setName] = useState('1');const [list, setList] = useState([1,2,3])const modifyName = () => {setName(name + '!')}// 父组件状态更新就会重新渲染,每次重新渲染foo函数都会被重新构建,并返回新的内存地址给foo,所以即使子组件用了memo包裹,子组件依然认为props的内存地址更新了,故子组件会重新渲染const foo = () => {console.log('foo')}return (<><div>{name}</div><button onClick={modifyName}> 点击修改名字 </button><EgOfMemo list={list} foo={foo} /></>)
}
上面代码解读一下:父组件状态更新就会重新渲染,每次重新渲染foo函数都会被重新构建,并返回新的内存地址给foo,所以即使子组件用了memo包裹,子组件依然认为props的内存地址更新了,故子组件会重新渲染。
也就是说,一旦传入给子组件的是函数类型,那么子组件使用memo包裹也没法让它避免没有意义的渲染,那应该怎么做呢?使用useCallback函数配合,下节博客会介绍!
3、不能每个组件都用memo包裹:如果真的每个组件都有被缓存的必要而且不会给项目带来破坏性问题,为什么react不直接把memo设为默认的呢。当然是因为每个都缓存的话,会给项目带来毁灭性的问题咯。
我们需要知道的是,缓存也需要成本。如果每个组件都进行缓存,会给浏览器带来非常非常大的负担。
所以在平常项目中,我们需要挑选一些经常被使用,经常会被重新渲染的组件去有目标的缓存他。而不是每一个组件都缓存一下。切记切记。
在某些情况下,使用 React.memo 可能会导致性能反而变差,例如当组件的渲染开销很小、props 变化频繁或者组件本身就是高阶组件时。 因此,在使用 React.memo 时,需要根据具体的场景进行评估和调整,以获得最优的性能表现。
最后,需要注意的是,React.memo 并不是万能的优化工具,它只能针对某些特定场景进行优化,而对于一些更复杂的性能问题,还需要使用其他更加专业的工具和技术进行优化。
总结
- 父组件中state(状态)改变,不受memo保护的子组件也会重新渲染
- 被memo函数包起来的组件只有本身的props被改变之后才会重新渲染
- memo只能进行浅比较来校验决定是否触发重新渲染。所以改变数组(对象)类型的props的时候记得返回一个全新的数组(对象)
- memo不是项目中所有的组件都需要包一下。包的太多反而会起反效果,我们需要选择那些经常被重新渲染的组件有选择性的去缓存。
React.memo 是 React 中一个很实用的工具,可以帮助我们优化组件的性能和渲染速度。它通过记忆组件的 props,并在下一次渲染时只有当 props 发生变化时才会重新渲染组件,从而大大提高了组件的渲染性能!