带着 memo 文章末尾的问题,继续来学习 useMemo、useCallback 的用法。

一、useMemo

在学习 useMemo 之前,我们先了解点基础知识:

  1. true === true // true
  2. false === false // true
  3. 1 === 1 // true
  4. 'k' === 'k' // true
  5. {} === {} // false
  6. [] === [] // false
  7. () => {} === () => {} // false

如果上述等式掌握了,下面的例子也应该不难理解了:

import React, { useState, memo, useMemo } from 'react';
import { Button, Card, Alert } from 'antd';

// 子组件
const Child = memo(({ textInfo }) => {
  console.log('渲染了子组件');
  return <Alert message={`${textInfo.text}`} />;
});

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('你好,世界!');

  // 按钮点击事件
  const handleClick = () => {
    setCount(count + 1);
  };

  // 获取textInfo对象
  const getTextInfo = () => {
    return { text:`${text}我的未来!` };
  }

  const textInfo = getTextInfo();

  return (
    <Card>
      <p>{count}</p>
      <p>
        <Button onClick={handleClick}>点击+1</Button>
      </p>
      <Child textInfo={textInfo} />
    </Card>
  );
};

export default Parent;

image.png
在开发过程中,我们可能会组装一些子组件参数(textInfo),但是我们发现 textInfo 一直是:

{
    text: "你好,世界!我的未来!"
}

但是当我们点击+1,发现子组件 Child 依然重新渲染了。当然了,我们的预期是子组件不会重新渲染,因为 textInfo 的值没有发生改变,所以子组件也无需渲染,只需渲染父组件即可。
image.png
但是问题出在哪里了呢?

当父组件渲染时,会重新生成新的 textInfo ,虽说父组件渲染前后,两次生成的 textInfo 的值是一样的,但是:

{ text:"你好,世界!我的未来!" } === { text:"你好,世界!我的未来!" }  // false

其实两者并不是恒等的,即子组件的属性值(props) 发生了变化,导致了子组件重新渲染。

既然发现了问题,那如何解决呢?

为了解决这个问题,useMemo 就出现了。

useMemo 和 useEffect 用法很相似,有两个参数。另外,useMemo 只会在某个依赖项发生更改时重新计算 memoized 值。

在本案例中使用 useMemo 优化后如下:

import React, { useState, memo, useMemo } from 'react';
import { Button, Card, Alert } from 'antd';

// 子组件
const Child = memo(({ textInfo }) => {
  console.log('渲染了子组件');
  return <Alert message={`${textInfo.text}`} />;
});

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('你好,世界!');

  // 按钮点击事件
  const handleClick = () => {
    setCount(count + 1);
  };

  // 获取textInfo对象
  const getTextInfo = () => {
    return { text:`${text}我的未来!` };
  }

  const textInfo = useMemo(getTextInfo, []);

  return (
    <Card>
      <p>{count}</p>
      <p>
        <Button onClick={handleClick}>点击+1</Button>
      </p>
      <Child textInfo={textInfo} />
    </Card>
  );
};

export default Parent;

通过用 useMemo 包裹 getTextInfo 函数后,其返回值是一个固定值,且不随父组件的重新渲染而改变。保证了子组件的属性值(props)—-textInfo 没有发生改变,即前后值恒等。所以尽管父组件随着点击+1而重新渲染,子组件也不会重新渲染。

二、useCallback

如果说 useMemo 是用来缓存值的,那么 useCallback 就是缓存函数的。

当我们给上述案例的子组件增加属性函数 onChange 时,如下:

import React, { useState, memo, useMemo } from 'react';
import { Button, Card, Alert } from 'antd';

// 子组件
const Child = memo(({ textInfo }) => {
  console.log('渲染了子组件');
  return <Alert message={`${textInfo.text}`} />;
});

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('你好,世界!');

  // 按钮点击事件
  const handleClick = () => {
    setCount(count + 1);
  };

  // 获取textInfo对象
  const getTextInfo = () => {
    return { text: `${text}我的未来!` };
  };

  const textInfo = useMemo(getTextInfo, []);

  // 子组件的回调函数
  const onChange = (value) => {
    setText(value);
  };

  return (
    <Card>
      <p>{count}</p>
      <p>
        <Button onClick={handleClick}>点击+1</Button>
      </p>
      <Child textInfo={textInfo} onChange={onChange} />
    </Card>
  );
};

export default Parent;

image.png
当我们点击+1时,子组件依然被渲染了,这也不是我们想要的。

当父组件渲染时,会重新定义 onChange 函数,导致渲染前后,两次 onChange 函数并不恒等!所以,导致子组件随父组件渲染而渲染。

同理 useMemo,用 useCallback 包裹后,如下:

import React, { useState, memo, useMemo, useCallback } from 'react';
import { Button, Card, Alert } from 'antd';

// 子组件
const Child = memo(({ textInfo }) => {
  console.log('渲染了子组件');
  return <Alert message={`${textInfo.text}`} />;
});

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('你好,世界!');

  // 按钮点击事件
  const handleClick = () => {
    setCount(count + 1);
  };

  // 获取textInfo对象
  const getTextInfo = () => {
    return { text: `${text}我的未来!` };
  };

  const textInfo = useMemo(getTextInfo, []);

  // 子组件的回调函数
  const onChange = useCallback((value) => {
    setText(value);
  }, []);

  return (
    <Card>
      <p>{count}</p>
      <p>
        <Button onClick={handleClick}>点击+1</Button>
      </p>
      <Child textInfo={textInfo} onChange={onChange} />
    </Card>
  );
};

export default Parent;

image.png
我们发现:当父组件重新渲染时,子组件并没有随之渲染。究其原因,useCallback 起到了缓存的作用,即便父组件渲染了,useCallback() 包裹的函数也不会重新生成,会返回上一次的函数引用。