你真的了解useMeme吗?

762 阅读3分钟

| 背景

useMemo是用来缓存数据的重要手段,合理使用可以在一定程度上的优化组件的性能。

每个useMemo天然的闭包 ,而闭包内存消耗大,不合理的使用会导致内存泄露(占着茅坑不拉屎),变成负优化

因此使用useMemo,是需要考量的。

| 什么时候使用

useMemo的存在有两个原因:

  1. 引用类型
  2. 高开销的计算

高开销的计算就不用说了,说一下引用类型

image.png

引用类型

先来看看一个demo

import React, { useState, useEffect } from 'react';

// 父组件
const Father = () => {
    const [num, setNum] = useState(0);
    const baz = 3;
    const bar = 'bar value';

    console.log('父页面渲染');
    return (
        <div>
            // 触发re-render
            <button
                onClick={() => {
                    setNum(num => (num += 1));
                }}
            >
                num++
            </button>
            
            <div>num:{num}</div>
            <Son bar={bar} baz={baz} />
        </div>
    );
};

// 子组件
function Son({ bar, baz }) {
    const options = { bar, baz };
    useEffect(() => {
        console.log(options, '子 useEffect hook执行了');
    }, [options]); // 如果bar或 baz变化时就执行

    return <div>我是子组件</div>;
}

引用类型总执行.gif

可以看到,即使bar, baz不改变,子组件的useEffect回调还是执行了 options 每次渲染被重新赋值导致引用地址改变,因此

引用地址改变 ==> 依赖改变 

意味着每次渲染都会调用子组件的useEffect回调

我们可以换种写法解决这个问题:

// 子组件
function Son({ bar, baz }) {
    useEffect(() => {
        const options = { bar, baz }; // 1.options定义在 useEffect里
        console.log(options, '子 useEffect hook执行了');
    }, [bar, baz]); // 2. 监听 bar 和 baz是否变化

    return <div>我是子组件</div>;
}

引用类型2.gif

如果bazbar是引用类型呢?

const Father = () => {
    const [num, setNum] = useState(0);
    const bar = { name: 'tz', age: 18 };
    const baz = [1, 2, 3];

    console.log('父页面渲染');
    return (
        ...
    );
};

引用类型3.gif
显然不起作用了
这时候useMeme派上用场了

const Father = () => {
    const [num, setNum] = useState(0);
    
    // 使用useMemo
    const bar = useMemo(() => ({ name: 'tz', age: 18 }), []);
    const baz = useMemo(() => [1, 2, 3], []);

    console.log('父页面渲染');
    return (
        ...
    );
};

引用类型4.gif

因此,使用useMemo的时候要考虑是否存在引用类型 or 高开销的计算

具体应用

  1. 依赖其他hook的引用类型
import React, {useState, useMemo} from "react";

const App = () => {
   const [someDatas, setSomeDatas] = useState([1, 2, 3]);

   // 依赖了其他 hook
   const memoData = useMemo(
       () =>
           someDatas.map(item => {
               return item + 100;
           }),
       [someDatas]
   );

   return <div>count: {memoData[0]}</div>;
};
  1. 父子组件传引用类型值
import React, { useState, useMemo, useEffect, memo } from 'react';

// 父组件
const Father = () => {
   const bar = useMemo(() => ({ name: 'tz', age: 18 }), []);
   const baz = useMemo(() => [1, 2, 3], []);

   console.log('父页面渲染');
   return (
       <div>
           <div>我是父组件</div>;

           // 父给子传递了引用类型
           <Son bar={bar} baz={baz} />
       </div>
   );
};

// 子组件
const Son = memo(({ bar, baz }) => {
   console.log('子页面渲染');
   useEffect(() => {
       const options = { bar, baz }; 
       console.log(options, '子 useEffect hook执行了');
   }, [bar, baz]); 

   return <div>我是子组件</div>;
});
  1. 多次高开销的计算
import React, {useMemo} from "react";

const App = () => {
    const ExpensiveCompute = useMemo(() => {
        let sum = 0;

        // 高开销运算
        for (let i = 0; i < 100000; i++) {
            sum += i;
        }
        return sum;
    }, []);

    return <div>count: {ExpensiveCompute}</div>;
};

高开销计算时useMemo使用对比
注:
第一个timer是开始计算时间(console.time)
第二个timer是脚本执行的时间(console.timeEnd)

使用useMemo image.png 不使用useMemo image.png

可见使用useMemo后执行时间大幅下降

| 总结

  1. useMemo本质上是一个闭包
  2. 使用时考虑引用类型 or 昂贵计算
  3. 依赖其他hook的引用类型时使用useMemo
  4. 父子组件传引用类型值时使用useMemo
  5. 高开销计算时使用useMemo

| 参考

# react hooks: 什么时候该用 useMemo / useCallback
# When to useMemo and useCallback