React中memo useMemo useCallback的用法和区别

在对 React 项目做性能优化的时候,memeo、useMemo、useCallback 三个API总是形影不离。

一、memo

1.memo作用

在 React 的渲染流程中,一般来说,父组件的某个状态发生改变,那么父组件会重新渲染,父组件所使用的所有子组件,都会强制渲染。而在某些场景中,子组件并没有使用父组件传入的没有发生更改的状态时,子组件重新渲染是没有必要的。因此有了 React.memo

2.memo 的使用

memo 是个高阶组件, 结合了 PurComponent 和 shouldComponentUpdate 功能,会对传入的 props 进行浅比较,来决定是否更新被包裹的组件

memo 接受两个参数:

  • WrapComponent:你要优化的组件
  • (prev, next) => boolean:通过对比 prev(旧 props),next(新 props)是否一致,返回 true(不更新)、false(更新)

注意:memo 只针对 props 来决定是否渲染,且是浅比较
现在我们来看一个的例子:

import { useState } from 'react';
const Child = () => (
    
{console.log('子组件渲染了')}
); function Parent() { const [status, setStatus] = useState(true); return (
); } export default Parent;

运行结果如下:


image.png

在上面的例子中,父组件中的状态 status和 Child 组件没有关系,当我点击按钮时,status 发生改变,此时父组件重新渲染,按钮文案变为off,控制台却打印出 "子组件又渲染" 的信息,说明子组件也跟着重新渲染了。而这肯定是不合理的,我们不希望子组件做无关的刷新,此时我们可以给子组件加上memo

import { useState, memo } from 'react';
const Child = memo(() => (
    
{console.log('子组件渲染了')}
)); function Parent() { const [status, setStatus] = useState(true); return (
); } export default Parent;
image.png

此时我们点击按钮,子组件不会被重新渲染

import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo((props) => (
    
{props.number} {console.log('子组件渲染了')}
)); Child.propTypes = { number: PropTypes.number, }; function Parent() { const [number, setNumber] = useState(1); return (
); } export default Parent;
image.png

在这个例子中,当我们点击按钮,传入子组件的number从1变为了2,子组件的props发生了改变,重新渲染

总而言之,如果组件被 memo 包裹,那么组件的 props 不发生改变时,组件不会重新渲染。这样,我们合理的使用 memo 就可以为我们的项目带来很大的性能优化

3.memo 的注意事项

memo 对于新旧 props 的比较默认是浅比较,当我们子组件接收的是一个引用类型的 props 的时候,可以自定义比较来决定是否需要使用缓存还是重新渲染

看下面的例子

import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo((props) => (
    
{`我叫${props.obj.name}`} {console.log('子组件渲染了')}
)); Child.propTypes = { obj: PropTypes.shape({ name: PropTypes.string, age: PropTypes.number, }), }; function Parent() { const [obj, setObj] = useState({ name: 'xxx', age: 18, }); return (
); } export default Parent;
image.png

我们点击按钮修改了age,子组件的props发生了变化,重新渲染。但是子组件中并没有用到age,我们不需要它重新渲染,这个时候我们可以使用memo的第二个参数来自定义校验规则

import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo(
    (props) => (
        
{`我叫${props.obj.name}`} {console.log('子组件渲染了')}
), // 新旧name相同就不重新渲染 (prev, next) => { return prev.obj.name === next.obj.name; }, ); Child.propTypes = { obj: PropTypes.shape({ name: PropTypes.string, age: PropTypes.number, }), }; function Parent() { const [obj, setObj] = useState({ name: 'xxx', age: 18, }); return (
); } export default Parent;
image.png

这个时候我们点击按钮修改age,子组件就不会重新渲染了。注意:默认情况下(没有自定义校验)即使引用对象的属性值没发生变化,但是地址改变了,也会引起子组件重新渲染,例如上述例子中使用setObj({...obj})

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

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

二、useMemo

1.useMemo 的作用

useMemo 它可以缓存一个结果,当这个缓存结果不变时,可以借此来进行性能优化。
看下面的例子

import { useState } from 'react';
const Parent = () => {
    const [number, setNumber] = useState(0);
    function addNumber() {
        setNumber(number + 1);
    }
    const result = () => {
        console.log('计算result');
        for (let i = 0; i < 10000; i++) {
            i.toString();
        }
        return 1000;
    };
    return (
        
result: {result()}
number: {number}
); }; export default Parent;
image.png

当我们点击按钮,number每次点击都会加1,result方法也会随着重新计算一遍,每次都要进行大量的for循环,很耗费性能,这种情况下我们可以使用useMemo来进行优化

2.useMemo 的使用

useMemo 接受两个参数:

  • callback:计算结果的执行函数
  • deps:相关依赖项数组

最终 useMemo 在执行了 callback 后,返回一个结果,这个结果就会被缓存起来。当 deps 依赖发生改变的时候,会重新执行 callback 计算并返回最新的结果,否则就使用缓存的结果
我们来把上面的例子用 useMemo 改造一下

import { useState, useMemo } from 'react';
const Parent = () => {
    const [number, setNumber] = useState(0);
    function addNumber() {
        setNumber(number + 1);
    }
    const result = useMemo(() => {
        console.log('计算result');
        for (let i = 0; i < 10000; i++) {
            i.toString();
        }
        return 1000;
    }, []);
    return (
        
result: {result}
number: {number}
); }; export default Parent;
image.png

现在不论我们怎么去改变number的值,result都不会重新运行,这样就达到了性能优化的目的
useMemo 并不是用的越多越好,缓存本身也需要开销,一些简单的计算方法就没必要使用useMemo

3.useMemo配合memo使用

import { useState, memo } from 'react';
const Child = memo(() => {
    console.log('子组件渲染');
    return 
子组件
; }); const Parent = () => { const [number, setNumber] = useState(0); function addNumber() { setNumber(number + 1); } const result = () => { console.log('计算result'); return 1000; }; return (
result: {result}
number: {number}
); }; export default Parent;
image.png

上面的例子中,result函数作为props传给了子组件,即使子组件被memo包裹着,但还是重新渲染了,这是因为,父组件重新渲染时,又创建了一个函数(或者说又开辟了一个内存地址)赋值给 result,而 memo 只做浅比较,发现地址改变了,所以子组件重新渲染,这个时候就需要使用 useMemo 来进行优化

import { useState, memo, useMemo } from 'react';
const Child = memo(() => {
    console.log('子组件渲染');
    return 
子组件
; }); const Parent = () => { const [number, setNumber] = useState(0); function addNumber() { setNumber(number + 1); } const result = useMemo(() => { console.log('计算result'); return 1000; }, []); return (
result: {result}
number: {number}
); }; export default Parent;
image.png

此时,再次点击按钮修改 number 后,子组件不会重新更新,达到了性能优化的目的

三、useCallback

1.useCallback 的作用

useCallback 类似于 useMemo,只不过 useCallback 用于缓存函数罢了,同样可以防止无关的刷新,对组件做出性能优化

2.useCallback 的使用

useCallback 同样接受两个参数:

  • callback:传入子组件的函数
  • deps:相关依赖项数组

最终 useCallback 会把传入的 callback 缓存起来。当 deps 依赖发生改变的时候,会重新缓存最新的 callback ,否则就使用缓存的结果

单独使用 useCallback 起不到优化的作用,反而会增加性能消耗,需要和 memo 一起使用

我们来把上面的例子用 useCallback 改造一下

import {
    useState,
    memo,
    useCallback,
} from 'react';
const Child = memo(() => {
    console.log('子组件渲染');
    return 
子组件
; }); const Parent = () => { const [number, setNumber] = useState(0); function addNumber() { setNumber(number + 1); } const result = useCallback(() => { console.log('计算result'); }, []); return (
result: {result}
number: {number}
); }; export default Parent;

点击按钮修改 number 后,子组件不会重新更新,达到了性能优化的目的

总结

memo:

  • 父组件重新渲染,没有被 memo 包裹的子组件也会重新渲染
  • 被 memo 包裹的组件只有在 props 改变后,才会重新渲染
  • memo 只会对新旧 props 做浅比较,所以对于引用类型的数据如果发生了更改,需要返回一个新的地址
  • memo 并不是用的越多越好,因为缓存本身也是需要开销的。如果每一个组件都用 memo 去包裹一下,那么对浏览器的开销就会很大,本末倒置了
  • 项目中可以针对刷新频率高的组件,根据实际情况,使用 memo 进行优化

useMemo:

  • useMemo 是对计算的结果进行缓存,当缓存结果不变时,会使用缓存结果
  • useMemo 并不是用的越多越好,对于耗时长、性能开销大的地方,可以使用 useMemo 来优化,但大多数情况下,计算结果的开销还没有使用 useMemo 的开销大,应视情况而定
  • 当父组件传了一个引用类型的结果 result 给子组件,且子组件用 memo 包裹时,需要使用 useMemo 对 result 进行缓存,因为 memo 只对 props 做浅比较,当父组件重新渲染时,会重新在内存中开辟一个地址赋值给 result,此时地址发生改变,子组件会重新渲染

useCallback:

  • useCallback 与 useMemo 类似,只不过是对函数进行缓存
  • useCallback 可以单独使用,但是单独使用的使用对性能优化并没有实质的提升,且父组件此时重新渲染,子组件同样会渲染
  • useCallback 需要配合 memo 一起使用,这样当父组件重新渲染时,缓存的函数的地址不会发生改变,memo 浅比较会认为 props 没有改变,因此子组件不会重新渲染

你可能感兴趣的:(React中memo useMemo useCallback的用法和区别)