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 = () => (
    <div>{console.log('子组件渲染了')}</div>
);
function Parent() {
    const [status, setStatus] = useState(true);
    return (
        <div>
            <Child />
            <button
                onClick={() =>
                    setStatus(!status)
                }>
                {status ? 'on' : 'off'}
            </button>
        </div>
    );
}
export default Parent;

运行结果如下:


image.png

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

import { useState, memo } from 'react';
const Child = memo(() => (
    <div>{console.log('子组件渲染了')}</div>
));
function Parent() {
    const [status, setStatus] = useState(true);
    return (
        <div>
            <Child />
            <button
                onClick={() =>
                    setStatus(!status)
                }>
                {status ? 'on' : 'off'}
            </button>
        </div>
    );
}
export default Parent;
image.png

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

import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo((props) => (
    <div>
        {props.number}
        {console.log('子组件渲染了')}
    </div>
));
Child.propTypes = {
    number: PropTypes.number,
};
function Parent() {
    const [number, setNumber] = useState(1);
    return (
        <div>
            <Child number={number} />
            <button onClick={() => setNumber(2)}>
                点击
            </button>
        </div>
    );
}
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) => (
    <div>
        {`我叫${props.obj.name}`}
        {console.log('子组件渲染了')}
    </div>
));
Child.propTypes = {
    obj: PropTypes.shape({
        name: PropTypes.string,
        age: PropTypes.number,
    }),
};
function Parent() {
    const [obj, setObj] = useState({
        name: 'xxx',
        age: 18,
    });
    return (
        <div>
            <Child obj={obj} />
            <button
                onClick={() => {
                    setObj({
                        name: 'xxx',
                        age: 19,
                    });
                }}>
                点击
            </button>
        </div>
    );
}
export default Parent;
image.png

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

import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo(
    (props) => (
        <div>
            {`我叫${props.obj.name}`}
            {console.log('子组件渲染了')}
        </div>
    ),
    // 新旧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 (
        <div>
            <Child obj={obj} />
            <button
                onClick={() => {
                    setObj({
                        name: 'xxx',
                        age: 19,
                    });
                }}>
                点击
            </button>
        </div>
    );
}
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 (
        <div>
            <div>result: {result()}</div>
            <div>number: {number}</div>
            <button onClick={() => addNumber()}>
                click
            </button>
        </div>
    );
};
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 (
        <div>
            <div>result: {result}</div>
            <div>number: {number}</div>
            <button onClick={() => addNumber()}>
                click
            </button>
        </div>
    );
};
export default Parent;
image.png

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

3.useMemo配合memo使用

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

image.png

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

import { useState, memo, useMemo } from 'react';
const Child = memo(() => {
    console.log('子组件渲染');
    return <div>子组件</div>;
});
const Parent = () => {
    const [number, setNumber] = useState(0);
    function addNumber() {
        setNumber(number + 1);
    }
    const result = useMemo(() => {
        console.log('计算result');
        return 1000;
    }, []);
    return (
        <div>
            <div>result: {result}</div>
            <div>number: {number}</div>
            <button onClick={() => addNumber()}>
                click
            </button>
            <Child result={result} />
        </div>
    );
};
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 <div>子组件</div>;
});
const Parent = () => {
    const [number, setNumber] = useState(0);
    function addNumber() {
        setNumber(number + 1);
    }
    const result = useCallback(() => {
        console.log('计算result');
    }, []);
    return (
        <div>
            <div>result: {result}</div>
            <div>number: {number}</div>
            <button onClick={() => addNumber()}>
                click
            </button>
            <Child result={result} />
        </div>
    );
};
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 没有改变,因此子组件不会重新渲染
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容