React基础 - React.memo,useCallback,useMemo,详解

前言

本文会从现象到本质讲解React.memouseMemouseCallback的功能性与应用场景,让小白少走弯路。

写给小白:当你不知道你这里为什么要使用这个方法的时候,你便不要使用它!

React.memo

ts 复制代码
function memo<P extends object>(
    Component: FunctionComponent<P>,
    propsAreEqual?: (prevProps: Readonly<P>, nextProps: Readonly<P>) => boolean,
): NamedExoticComponent<P>;

function memo<T extends ComponentType<any>>(
    Component: T,
    propsAreEqual?: (prevProps: Readonly<ComponentProps<T>>, nextProps: Readonly<ComponentProps<T>>) => boolean,
): MemoExoticComponent<T>;

先来一个简单的demo

父组件两个状态,子组件接收其中一个并直接打印随机数来判断子组件渲染情况;

如果子组件随机数变化了,则表明子组件重新渲染了。

tsx 复制代码
function Demo1() {
  const [count, setCount] = useState(0)
  const [childrenCount, setChildrenCount] = useState(0)
  return (
    <div>
      <Button onClick={() => setCount(count + 1)}>count + 1</Button> <br />
      <Button onClick={() => setChildrenCount(childrenCount + 1)}>childrenCount + 1</Button> <br />
      <Demo1_1 count={childrenCount} />
      <Demo1_2 count={childrenCount} />
    </div>
  )
}

const Demo1_1 = (props: {count: number}) => {
  return <div>Demo1_1: { Math.random() }</div>
}

const Demo1_2 = React.memo((props: {count: number}) => {
  return <div>Demo1_2: { Math.random() }</div>
})

我们通过分别点击两个按钮可以看到,Demo1_1在父组件任意状态变化的情况下都进行了重渲染,Demo1_2仅在接收的props变化的时候才会进行重渲染;

如此,我们便可以使用React.memo来减少父级状态变化给子组件带来没必要的渲染,从而优化性能。

React.memo 第二参数

React.memo支持传入第二个参数propsAreEqual,一个函数,入参为变化前后props,出参为布尔值。我们添加 Demo1_3,并再次修改父组件状态:

tsx 复制代码
const Demo1_3 = React.memo((props: {count: number}) => {
  return <div>Demo1_3: {Math.random()} | count: {props.count}</div>
}, (prevProps, nextProps) => { 
  return nextProps.count > 3
})

我们通过修改父组件childrenCount状态可以看到,propsAreEqual返回值为true时,子组件会认为prevPropsnextProps是等价的,便不会重渲染,从而优化性能。

注意

prevPropsnextProps 本身是不全等的!如果返回 prevProps === nextProps 则结果永远为false

需要按字段逐个判断,如:prevProps.count === nextProps.count && ...

第二参数通常不需要手动维护,默认会逐个比较props中每个key

useCallback

ts 复制代码
function useCallback<T extends Function>(callback: T, deps: React.DependencyList): T

可以看到,useCallback接受一个函数和一组依赖,并返回一个函数

先来一个简单的Demo

父组件自身有一个可改变的状态,子组件接收一个方法props,分别按内联、定义函数、useCallback 三种方式传入子组件,子组件渲染随机数。

如果子组件随机数变化,则表示子组件进行了重渲染。

tsx 复制代码
function Demo2() {
  const [count, setCount] = useState(0)
  const handleClick = () => { }

  const handleClickCallback = useCallback(() => { }, [])

  return (
    <div>
      <Button onClick={() => setCount(count + 1)}>count + 1</Button> <br />
      <Demo2_1 mark={1} onClick={() => { }}/>
      <Demo2_1 mark={2} onClick={handleClick} />
      <Demo2_1 mark={3} onClick={handleClickCallback} />
    </div>
  )
}

const Demo2_1 = React.memo((props: {onClick: () => void, mark: number}) => {
  return <div>mark: {props.mark} { Math.random() }</div>
})

我们通过点击按钮可以看到,在父组件任意状态变化的情况下mark1,mark2进行了重渲染,mark3不会进行重渲染;

根据React.memo的执行逻辑可以推断,是父组件更新state后,导致传入子组件的onClick参数变化,从而触发子组件的重渲染。

  • count变化会导致Demo2重新执行
  • mark1子组件会在Demo2执行的时候,接收到一个全新匿名函数作为props
  • mark2子组件会在Demo2执行的时候,接收到一个参数handleClick指向一个全新匿名函数作为props
  • mark3子组件会在Demo2执行的时候,接收到一个参数handleClickCallback指向useCallback执行创建的函数缓存,函数缓存不更新,则handleClickCallback指向不变。

如此便知道,我们可以通过useCallback创建函数缓存,来避免函数重复创建和销毁带来的开销,同时,将函数缓存传入React.memo包装的子组件,可以避免函数指向变化带来子组件额外的渲染,从而提升性能。

useCallback 第二参数

useCallback支持传入第二个参数deps: React.DependencyList,一个数组,如果不传递,会在每次useCallback执行的时候创建一个全新的函数缓存,如此,便与不使用useCallback表现的现象一致,同时还会增加创建和销毁函数缓存的开销,造成反向优化的效果。

传入第二参数后,useCallback执行时会根据dep元素变化而更新返回的函数缓存。

我们写一个Demo3,分别在依赖项中传入和不传入状态依赖,传入的事件打印父级状态

tsx 复制代码
function Demo3() {
  const [childrenCount, setChildrenCount] = useState(0)

  const handleClickCallback4 = useCallback(() => console.log(childrenCount), [childrenCount])
  const handleClickCallback5 = useCallback(() => console.log(childrenCount), [])

  return (
    <div>
      <Button onClick={() => setChildrenCount(childrenCount + 1)}>childrenCount + 1 count={childrenCount}</Button> <br />
      <Demo3_1 mark={4} onClick={handleClickCallback4} />
      <Demo3_1 mark={5} onClick={handleClickCallback5} />
    </div>
  )
}
const Demo3_1 = React.memo((props: {onClick: () => void, mark: number}) => {
  return <div onClick={props.onClick}>mark: {props.mark} { Math.random() }</div>
})

我们通过修改父级状态后,再点击子组件查看打印结果可以得到如下现象:

  • mark4在每次父组件状态更新后,点击子组件可以正常取到当前父组件的childrenCount,但是子组件会随着父组件状态更新而重渲染
  • mark5不会随着父组件状态更新重渲染,但是点击子组件后,也没办法获取到当前父组件的childrenCount,只能获取初始化的值。

如此看来,在子组件未用到父组件状态,但接收的方法用到父组件状态的情况下,似乎无法做到有效渲染优化。

实际上,针对这个场景,并不是没有解决方案:

通常方案:useRef 缓存state

使用ref缓存最新state值,并在state更新的时候同步更新ref值。在useCallback里从ref中取值即可

tsx 复制代码
const [childrenCount, stateSetChildrenCount] = useState(0)
const childrenCountRef = useRef(0)

const setChildrenCount = useCallback((value: number) => {
    childrenCountRef.current = value
    stateSetChildrenCount(value)
}, [])

const handleClickCallback6 = useCallback(() => { 
    console.log(childrenCountRef.current)
}, [])
邪修方案:setState中取最新state。但是:不推荐!不推荐!不推荐!

使用setState的回调函数来获取最新state

tsx 复制代码
const [childrenCount, setChildrenCount] = useState(0)

const handleClickCallback7 = useCallback(() => { 
    setChildrenCount(prevState => { 
      console.log(prevState)
      return prevState
    })
}, [])

useMemo

ts 复制代码
function useMemo<T>(factory: () => T, deps: DependencyList): T;

可以看到,useMemo接受一个函数和一组依赖,并返回函数的返回值。

聪明的小伙伴会发现,如果 T extends Function ,那么useMemo的返回便与useCallback的返回一般无二了。

事实也是如此:如下写法 someFunction的实际效果是一样的,但是:不推荐!不推荐!不推荐!

tsx 复制代码
const someFunction = useCallback((params: any) => {}, [])
const someFunction = useMemo(() => (params: any) => {}, [])

实际使用中,如果返回值为函数类型,便用useCallback,其它类型再使用useMemo。如此,便与期望一致。

另外,请不要,请不要,请不要useMemo中执行setState操作,如有需要,请移步useEffectuseCallback,这是个规范性的问题。

其它使用方式可参考上文useCallback

易错点与矛盾点

  • 那是不是所有的react函数组件都需要使用React.memo包装一遍?

    • 视情况而定,React.memo本质是一个高阶函数,本身便会带来有限的性能开销。
    • 如果是一个有很多state的大页面,包裹一个有很多state的大组件,使用React.memo会减少大量的渲染开销。
    • 如果是一个无状态页面,包裹一个子组件,自然没必要使用React.memo
    • 虽然是两个极端,实际还得通过实际情况做判断。如果上述示例中,子组件本身就存在跟随父组件重渲染的需求:实时更新随机数。那便不能使用React.memo
    • 大部分情况,小页面都不会复杂到必须使用React.memo来优化性能的地步。
  • 那是不是所有的react函数组件内部变量都需要使用useCallbackuseMemo

    • 视情况而定,同理,这些hook本身便会带来有限的性能开销。
    • 如有一长段数据处理方法,需要页面指定state变化的时候才执行,则用useMemo封装会减少大量重复计算开销。
    • 如父组件有大量state变化,需要传递一个简单的方法作为props到子组件,则用useCallback封装会减少大量函数定义和销毁。
    • 如果你对这些hook使用并不熟练,则不需要使用这些hook,牺牲性能带来系统稳定。
  • 那是不是useCallbackuseMemo中所有使用到的state都需要作为第二参数依赖项?

    • 并不是,本质上来说,使用到的state和第二参数的依赖项并没有直接关系,这里也是最容易出问题的地方。一般是因为漏加依赖导致执行结果与预期不符。但是全加又容易导致达不到性能优化预期。
    • 依赖项,决定了缓存对象的更新节点。需要更新则需要依赖,反之则不需要。
相关推荐
小只笨笨狗~36 分钟前
el-dialog宽度根据内容撑开
前端·vue.js·elementui
weixin_4903543440 分钟前
Vue设计与实现
前端·javascript·vue.js
GISer_Jing1 小时前
React过渡更新:优化渲染性能的秘密
javascript·react.js·ecmascript
烛阴2 小时前
带你用TS彻底搞懂ECS架构模式
前端·javascript·typescript
卓码软件测评3 小时前
【第三方网站运行环境测试:服务器配置(如Nginx/Apache)的WEB安全测试重点】
运维·服务器·前端·网络协议·nginx·web安全·apache
龙在天3 小时前
前端不求人系列 之 一条命令自动部署项目
前端
开开心心就好3 小时前
PDF转长图工具,一键多页转图片
java·服务器·前端·数据库·人工智能·pdf·推荐算法
国家不保护废物3 小时前
10万条数据插入页面:从性能优化到虚拟列表的终极方案
前端·面试·性能优化
文心快码BaiduComate3 小时前
七夕,画个动态星空送给Ta
前端·后端·程序员
web前端1233 小时前
# 多行文本溢出实现方法
前端·javascript