美文网首页
React中memo useMemo useCallback的用

React中memo useMemo useCallback的用

作者: TurnHug | 来源:发表于2023-11-01 22:39 被阅读0次

    在对 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 没有改变,因此子组件不会重新渲染

    相关文章

      网友评论

          本文标题:React中memo useMemo useCallback的用

          本文链接:https://www.haomeiwen.com/subject/wlwbidtx.html