memo()、useCallback()、useMemo()在性能优化中的用法解析与归纳

747 阅读7分钟

关于性能优化主要是避免多余无用渲染;

我们知道(1.state;2.props 3.context)这三者任意一个变化会造成页面重新渲染;当结构为父子组件时其中父组件渲染,子组件默认也会重新渲染,带着这些前置知识我们来继续分析

使用方法及原理

这三个方法作为性能优化的使用步骤

1.单独使用 memo()

使用React.memo()包裹子组建后,每次父组建渲染子组建则会进行props对象属性的浅比较,若相等则子组件不进行渲染;但是因为是浅比较,所以当props为引用类型数据时,父组件渲染,函数或者对象会被重新创建,导致引用地址改变,虽然此时代码没变,但是他们已经不相等; 优化将会无效(这里则会埋下2个坑:1.props值为函数时优化将无效,2为对象时优化将无效)

2.memo()配合useCallback()

memo()配合 useCallback():解决1.props值为函数时优化将无效的坑

具体原理为useCallback()缓存函数,父组件更新,只要依赖项不变,函数即可不更新。

3.memo()配合useMemo()

memo()配合 useMemo():解决1.props值为对象时优化将无效的坑

具体原理为useMemo()缓存对象数据,父组件更新,只要依赖项不变,对象即可不更新。

面试如何一句话回答清楚?

答:我们知道react中父组件更新渲染,子组件默认也会更新渲染,这会导致多余的无用渲染,造成性能问题。可以使用memo缓存子组件,这样只有当子组件props改变它才会重新渲染,但是这里的对比是浅比较,当props为引用类型数据时需要去缓存引用类型的props才能保证他们在父组件每次渲染后相等,如使用useCallback缓存函数props,使用useMemo缓存对象类型数据。这样就能做到只有props变化子组件才重新渲染。

详细分析

memo()、useCallback()、useMemo() 三者都是用于解决父子嵌套,父组件状态变化导致子组件也会做无用的重新渲染问题。主要是解决props变化带来的问题,再往下挖就是引用类型数据的比较问题

关于props的问题:

默认情况下,父组建渲染,子组建也会进行渲染; 子组建加了React.memo()高阶函数包裹后react会对props是否变化的判断使用的是全等对比即newProps===oldProps(这一点可以在源码中看到);假如props是一个对象(如父组建中定义的函数),每次父组建重新渲染函数会重新创建,地址也会改变,props中的对象对比就会不相等,此时react则认为props改变了则会重新渲染;

我们得出结论:使用React.memo()包裹后则会进行props对象属性的浅比较(这里埋下2个坑:props值为函数或对象时优化将无效,需配合优化hook)

  • memo()包裹子组件:用于解决props值为基础数据类型

  • memo()包裹子组件 + useCallback():用于解决props值为函数

  • memo()包裹子组件 + useMemo():用于解决props值为对象

解析

1.子组件不包含状态值或状态值是基础类型(react.memo())

当父组件未给子组件传递任何属性,我们可以通过React.memo()来控制函数子组件的渲染

  • 注意: 当我们传值给子组件时,这时使用memo()函数就对控制组件的更新不起作用了
import { useState } from "react";
import { Child } from "./child";

export const Parent = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <Child />
    </div>
  );
};

import {memo} from 'react'
export const Child=memo(()=>{
    consloe.log('渲染了')
    return <div>子组件</div>
})
//使用memo()包裹后的组件,在Parent组件重新渲染更新时,Child组件将不会
2.父组件向子组件传递了一个函数(useCallback()+memo()包裹子组件)

useCallback是用于缓存函数的,

从1中我们知道由于props是全等比较或浅比较所以当value是函数时我们可以用useCallback来解决函数地址改变造成props改变的问题,

如果useCallback第二个参数依赖项没有发生改变,则直接返回缓存结果,不会重新渲染

  • 注意: 如果直接使用useState解构的setName()传给子组件, 子组件将不会重复渲染,因为解构出来的是一个memoized 函数。

import { useCallback, useState } from "react";
import { Child } from "./child";
​
export const Parent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("小明");
  const increment = () => setCount(count + 1);
//使用useCallback钩子包裹的回调函数是memoized函数,
//他初次调用该函数时,缓存参数和计算结果,再次调用这个函数时,
//如果第二个参数依赖项没有发生改变,则直接返回缓存结果,不会重新渲染
  const onClick = useCallback((name: string) => {
    setName(name);
  }, []);
​
  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      // 子组件需要是memo 包裹的组件
      <Child name={name} onClick={onClick} />
    </div>
  );
};
3.父组件向子组件传递了一个对象(React.useMemo()+memo()包裹子组件)

useMemo()单独使用时是用来缓存变量的,返回的是一个 memoized 值。

从1中我们知道由于props是全等比较或浅比较所以当value是对象时我们要用useMemo来解决地址改变问题

如果useMemo第二个参数依赖项没有发生改变,则直接返回缓存结果,不会重新渲染;如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。


import { useMemo, useState } from "react";
import { Child } from "./child";
​
export const Parent = () => {
  const [count, setCount] = useState(0);
  // const [userInfo, setUserInfo] = useState({ name: "小明", age: 18 });
  const increment = () => setCount(count + 1);
  const userInfo = useMemo(() => ({ name: "小明", age: 18 }), []);
  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      //子组件需要是memo 包裹的组件
      <Child userInfo={userInfo} />
    </div>
  );
};

归纳一波

三者都是用于解决子组件做多余渲染的性能问题

  • React.memo():用于处理组件父向子 未传递状态或传递的是基础类型数据 用法: ------- memo()直接包裹子组件
  • useCallback():用于组件父向子传递了一个 函数 用法:------- useCallback()缓存函数
  • useMemo():用于组件父向子传递了一个 对象 用法:------- useMemo()缓存对象

useCallback useMemo 都是为了解决父组件重新渲染时,未缓存的引用类型数据(函数,对象)将会重新指向新的地址,导致子组件理解props变化了,导致重新渲染(虽然代码未改变,但是此时已经是一个新的函数),使用他们来缓存对象,当第二个参数内的状态变化时才重新计算更新状态与函数。

实际项目中优化策略

既然 以上三者可以对组件进行性能优化,那能不能所有组件都用 memo 包裹呢?

答案肯定是否定的。

因为缓存本身也是需要开销的。如果每一个组件都用 memo 去包裹一下,那么对浏览器的开销就会很大,本末倒置了。

所以我们应该选择性的用 memo 包裹组件,而不是滥用。

7e1aff6f1bf36076bb899e2742d2276.png

用法总结

如果子组件用了 memo,那给它传递的对象、函数类的 props 就需要用 useMemo、useCallback 包裹,否则,每次 props 都会变,memo 就没用了。

反之,如果 props 使用 useMemo、useCallback,但是子组件没有被 memo 包裹,那也没意义,因为不管 props 变没变都会重新渲染,只是做了无用功。

memo + useCallback、useMemo 是搭配着来的,少了任何一方,都会使优化失效。

但 useMemo 和 useCallback 也不只是配合 memo 用的:

比如有个值的计算,需要很大的计算量,你不想每次都算,这时候也可以用 useMemo 来缓存。