memo、useMemo及useCallback解析

memo、useMemo及useCallback解析

前言

在hooks诞生之前,如果组件包含内部 state,我们都是基于 class 的形式来创建组件。

在react中,性能优化的点在于:

  1. 调用 setState,就会触发组件的重新渲染,无论前后 state 是否相同
  2. 父组件更新,子组件也会自动更新

基于上面的两点,我们通常的解决方案是:使用 immutable 进行比较,在不相等的时候调用 setState, 在 shouldComponentUpdate 中判断前后的 props 和 state,如果没有变化,则返回 false 来阻止更新。

在 hooks 出来之后,函数组件中没有 shouldComponentUpdate 生命周期,我们无法通过判断前后状态来决定是否更新。useEffect 不再区分 mount update 两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。

对比

我们先简单的看一下useMemo和useCallback的调用签名:

function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T; function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
复制代码

useCallback 和 useMemo 的参数跟 useEffect 一致,他们之间最大的区别有是 useEffect 会用于处理副作用,而前两个hooks不能。

useCallback 和 useMemo 都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo 返回缓存的 变量useCallback 返回缓存的 函数

React.memo()

在 class 组件时代,为了性能优化我们经常会选择使用 PureComponent,每次对 props 进行一次浅比较,当然,除了 PureComponent 外,我们还可以在 shouldComponentUpdate 中进行更深层次的控制。

在 Function 组件中, React 贴心的提供了 React.memo 这个 HOC(高阶组件),与 PureComponent 很相似,但是是专门给 Function Component 提供的,对 Class Component 并不适用。

但是相比于 PureComponent ,React.memo() 可以支持指定一个参数,可以相当于 shouldComponentUpdate 的作用,因此 React.memo() 相对于 PureComponent 来说,用法更加方便。

(当然,如果自己封装一个 HOC,并且内部实现 PureComponent + shouldComponentUpdate 的结合使用,肯定也是 OK 的,在以往项目中,这样使用的方式还挺多)

首先看下 React.memo() 的使用方式:

function MyComponent(props) {
  /* render using props */
}
function areEqual(prevProps, nextProps) {
  /*
  return true if passing nextProps to render would return
  the same result as passing prevProps to render,
  otherwise return false
  */
}
export default React.memo(MyComponent, areEqual);
复制代码

使用方式很简单,在 Function Component 之外,在声明一个 areEqual 方法来判断两次 props 有什么不同,如果第二个参数不传递,则默认只会进行 props 的浅比较

最终 export 的组件,就是 React.memo() 包装之后的组件。

实例:

  • index.js:父组件

  • Child.js:子组件

  • ChildMemo.js:使用 React.memo 包装过的子组件

index.js

import React, { useState, } from 'react';
import Child from './Child';
import ChildMemo from './Child-memo';

export default (props = {}) => {
    const [step, setStep] = useState(0);
    const [count, setCount] = useState(0);
    const [number, setNumber] = useState(0);

    const handleSetStep = () => {
        setStep(step + 1);
    }

    const handleSetCount = () => {
        setCount(count + 1);
    }

    const handleCalNumber = () => {
        setNumber(count + step);
    }

    return (
        <div>
            <button onClick={handleSetStep}>step is : {step} </button>
            <button onClick={handleSetCount}>count is : {count} </button>
            <button onClick={handleCalNumber}>numberis : {number} </button>
            <hr />
            <Child step={step} count={count} number={number} /> <hr />
            <ChildMemo step={step} count={count} number={number} />
        </div>
    );
}
复制代码

child.js

这个子组件本身没有任何逻辑,也没有任何包装,就是渲染了父组件传递过来的 props.number

需要注意的是,子组件中并没有使用到 props.step 和 props.count,但是一旦 props.step 发生了变化就会触发重新渲染。

import React from 'react';

export default (props = {}) => {
    console.log(`--- re-render ---`);
    return (
        <div>
            {/* <p>step is : {props.step}</p> */}
            {/* <p>count is : {props.count}</p> */}
            <p>number is : {props.number}</p>
        </div>
    );
};
复制代码

childMemo.js

这个子组件使用了 React.memo 进行了包装,并且通过 isEqual 方法判断只有当两次 props 的 number 的时候才会重新触发渲染,否则 console.log 也不会执行。

import React, { memo, } from 'react';

const isEqual = (prevProps, nextProps) => {
    if (prevProps.number !== nextProps.number) {
        return false;
    }
    return true;
}

export default memo((props = {}) => {
    console.log(`--- memo re-render ---`);
    return (
        <div>
            {/* <p>step is : {props.step}</p> */}
            {/* <p>count is : {props.count}</p> */}
            <p>number is : {props.number}</p>
        </div>
    );
}, isEqual);
复制代码

效果对比

 

 

 

通过上图可以发现,在点击 step 和 count 的时候,props.step 和 props.count 都发生了变化,因此 Child.js 这个子组件每次都在重新执行渲染(----re-render----),即使没有用到这两个 props。

而这种情况下,ChildMemo.js 则不会重新进行 re-render。

只有当 props.number 发生变化的时候,ChildMemo.js 和 Child.js 表现是一致的。

从上面可以看出,React.memo() 的第二个方法在某种特定需求下,是必须存在的。 因为在实验的场景中,我们能够看得出来,即使我使用 React.memo 包装了 Child.js,也会一直触发重新渲染,因为 props 浅比较肯定是发生了变化。

React.useMemo() 细粒度性能优化

上面 React.memo() 的使用我们可以发现,最终都是在最外层包装了整个组件,并且需要手动写一个方法比较那些具体的 props 不相同才进行 re-render。

而在某些场景下,我们只是希望 component 的部分不要进行 re-render,而不是整个 component 不要 re-render,也就是要实现 局部 Pure 功能。

useMemo() 基本用法如下:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码

useMemo() 返回的是一个 memoized 值,只有当依赖项(比如上面的 a,b 发生变化的时候,才会重新计算这个 memoized 值)

memoized 值不变的情况下,不会重新触发渲染逻辑。

说起渲染逻辑,需要记住的是 useMemo() 是在 render 期间执行的,所以不能进行一些额外的副操作,比如网络请求等。

如果没有提供依赖数组(上面的 [a,b])则每次都会重新计算 memoized 值,也就会 re-redner

上面的代码中新增一个 Child-useMemo.js 子组件如下:

import React, { useMemo } from 'react';

export default (props = {}) => {
    console.log(`--- component re-render ---`);
    return useMemo(() => {
        console.log(`--- useMemo re-render ---`);
        return <div>
            {/* <p>step is : {props.step}</p> */}
            {/* <p>count is : {props.count}</p> */}
            <p>number is : {props.number}</p>
        </div>
    }, [props.number]);
}
复制代码

与上面唯一的区别是使用的 useMemo() 包装的是 return 部分渲染的逻辑,并且声明依赖了 props.number,其他的并未发生变化。

效果对比:

 

 

 

上面图中我们可以发现,父组件每次更新 step/count 都会触发 useMemo 封装的子组件的 re-render,但是 number 没有变化,说明并没有重新触发 HTML 部分 re-render

只有当依赖的 props.number 发生变化的时候,才会重新触发 useMemo() 包装的里面的 re-render

React.useCallback()

讲完了useMemo,接下来是 useCallback。useCallback 跟 useMemo 比较类似,但它返回的是缓存的函数。我们看一下最简单的用法:

const fnA = useCallback(fnB, [a])
复制代码

实例:

import React, { useState, useCallback } from 'react';
import Button from './Button';

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const handleClickButton1 = () => {
    setCount1(count1 + 1);
  };

  const handleClickButton2 = useCallback(() => {
    setCount2(count2 + 1);
  }, [count2]);

  return (
    <div>
      <div>
        <Button onClickButton={handleClickButton1}>Button1</Button>
      </div>
      <div>
        <Button onClickButton={handleClickButton2}>Button2</Button>
      </div>
    </div>
  );
}
复制代码

Button组件

// Button.jsx
import React from 'react';

const Button = ({ onClickButton, children }) => {
  return (
    <>
      <button onClick={onClickButton}>{children}</button>
      <span>{Math.random()}</span>
    </>
  );
};

export default React.memo(Button);
复制代码

这里或许会注意到 React.memo 这个方法,此方法内会对 props 做一个浅层比较,如果如果 props 没有发生改变,则不会重新渲染此组件。

上面的 Button 组件都需要一个 onClickButton 的 props ,尽管组件内部有用 React.memo 来做优化,但是我们声明的 handleClickButton1 是直接定义了一个方法,这也就导致只要是父组件重新渲染(状态或者props更新)就会导致这里声明出一个新的方法,新的方法和旧的方法尽管长的一样,但是依旧是两个不同的对象,React.memo 对比后发现对象 props 改变,就重新渲染了。

const handleClickButton2 = useCallback(() => {
  setCount2(count2 + 1);
}, [count2]);
复制代码

上述代码我们的方法使用 useCallback 包装了一层,并且后面还传入了一个 [count2] 变量,这里 useCallback 就会根据 count2 是否发生变化,从而决定是否返回一个新的函数,函数内部作用域也随之更新。

由于我们的这个方法只依赖了 count2 这个变量,而且 count2 只在点击 Button2 后才会更新 handleClickButton2,所以就导致了我们点击 Button1 不重新渲染 Button2 的内容。

总结

  1. 在子组件不需要父组件的值和函数的情况下,只需要使用 memo 函数包裹子组件即可。

  2. 如果有函数传递给子组件,使用 useCallback

  3. 如果有值传递给子组件,使用 useMemo

  4. useEffectuseMemouseCallback 都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(stateprops),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用 ref 来访问。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值