React Hooks之useCallback useMemo memo的用法

616 阅读5分钟

背景

React中当组件的属性或者状态发生变化时,React 会调用组件的 render() 方法重新渲染组件。
以下是一些会导致组件重新渲染的情况:

  1. 组件的 props 发生变化;
  2. 组件的 state 发生变化;
  3. 父组件重新渲染
  4. 使用 forceUpdate() 强制重新渲染;

React 中,每次状态或属性变化时,组件都会重新渲染。如果一个组件引入很多子组件,当父组件状态变化,整体重新渲染就非常消耗性能。

一、React.memo()

概述

React.memo()是一个高阶组件,用于在某种特定的条件下优化React组件的性能。它类似于类组件的React.PureComponent。它接收一个函数组件,并返回一个新的组件。它的作用就是当父组件传给当前被memo()包裹的组件的props值没有发生变化时,该组件不会重新渲染。

用法

    import React from 'react';
    import 'antd/dist/antd.css';
    import './index.css';
    import { Input, Button, Space } from 'antd';

    const App: React.FC = () => {
      console.log('App Render');
      const [value, setValue] = React.useState('');

      const [list, setList] = React.useState([
        {
          name: '鸡腿堡',
          id: 0,
        },
        {
          name: '牛肉堡',
          id: 1,
        },
      ]);

      const handleChange = (e) => {
        const {
          target: { value },
        } = e;
        setValue(value);
      };

      const handleAdd = () => {
        setList([...list, { id: Number(new Date()), name: value }]);
      };

      return (
        <div>
          <Space>
            <Input value={value} onChange={handleChange} placeholder="请输入" />
            <Button type="primary" onClick={handleAdd}>
              添加
            </Button>
          </Space>
          <List list={list} />
        </div>
      );
    };

    const List = (props) => {
      console.log('List Render');
      const { list } = props;
      return list.map((item) => (
        <p style={{ marginTop: 10, padding: 10, background: 'pink' }} key={item.id}>
          <Space>
            {item.name}
            <Button type="link">删除</Button>
          </Space>
        </p>
      ));
    };

    export default App;

在上面的代码中当我们改变输入框的值时,会触发state的变化,从而导致App组件重新渲染;然而List组件接收到App组件的list并无变化,也会导致重新渲染。如下图所示:

GIF 2023-4-14 0-22-15.gif

为了防止组件的频繁重新渲染,可以使用React.memo()将组件包裹,这样memo会判断如果组件没有props的变化时,不会重新渲染组件,代码如下:

// 给List组件添加memo包裹
const List = React.memo((props) => {
  console.log('List Render');
  const { list } = props;
  return list.map((item) => (
    <p style={{ marginTop: 10, padding: 10, background: 'pink' }} key={item.id}>
      <Space>
        {item.name}
        <Button type="link">删除</Button>
      </Space>
    </p>
  ));
});

这样在App组件重新渲染时,List组件不会重新渲染,达到优化性能的目的。

GIF 2023-4-14 0-30-50.gif

注: 需要注意的是,React.memo()是对props的浅层比较,如果父组件传给子组件的是引用类型,只会比较它们的引用地址,不会进行深层次的比较。所以当我们传给子组件的是函数或者对象是,这时候就需要用到useCallbackuseMemo进行优化;

使用React.memo并不是一定会提升性能,只有当组件的渲染成本props比较成本高得多时,才会有明显的性能提升。如果组件的渲染成本很低,而props比较成本很高,那么使用React.memo反而会降低性能。

因此,在使用React.memo时,需要根据实际情况进行衡量,综合考虑组件的渲染成本和props比较成本,来判断是否使用React.memo

二、React.useCallback()

概述

useCallbackReact的一个Hook函数,用来缓存函数的引用,作用就是避免函数的重复创建
实际场景就是当父组件传给子组件一个函数时,父组件的渲染会造成该函数的重新创建,函数引用发生了变化,子组件判断props发生了变化导致子组件也重新渲染。

用法

useCallback 将一个函数和一个依赖项数组作为参数,当依赖项发生变化时,才会重新创建函数。否则,返回缓存的函数引用,这样就能避免不必要的函数创建和渲染。

// App组件
  const handleRemove = (id) => {
    setList([...list.filter((f) => f.id !== id)]);
  };
  <List list={list} remove={handleRemove} />
  
// List组件
const List = React.memo((props) => {
  console.log('List Render');
  const { list, remove } = props;
  return list.map((item) => (
    <p style={{ marginTop: 10, padding: 10, background: 'pink' }} key={item.id}>
      <Space>
        {item.name}
        <Button type="link" onClick={() => remove(item.id)}>
          删除
        </Button>
      </Space>
    </p>
  ));
});

在上面的代码中,我们创建一个删除函数并传给子组件,由于函数是引用类型,在App组件重新渲染时,函数会重新创建,即便子组件有memo的包裹,函数引用的改变会造成props的变化,继而子组件重新渲染,如下图所示:

222222.gif 为了防止这种组件的重复渲染影响性能,这个时候就可以用到useCallback,代码如下所示:

  const handleRemove = React.useCallback(
    (id) => {
      setList([...list.filter((f) => f.id !== id)]);
    },
    [list] // 只有当依赖的list变化时,函数才会重新创建
  );

这时,输入框变化时并不会造成子组件的更新,只有当我们添加元素或者删除元素时,函数引用才会变化,继而触发子组件的更新,如下图所示:

444444.gif

三、React.useMemo()

概述

useMemo用法和useCallback相似,都是用于缓存避免组件的重复渲染。它们的不同之处在于useMemo返回缓存的计算结果,而useCallback返回一个缓存的函数。

用法

缓存计算结果
当组件需要进行大量计算时,使用useMemo可以避免重复计算,提高组件的性能。 在上述例子中,我们给每一个汉堡添加价格;

  const [list, setList] = React.useState([
    {
      name: '鸡腿堡',
      id: 0,
      price: 10,
    },
    {
      name: '牛肉堡',
      id: 1,
      price: 20,
    },
  ]);
  
  // 计算总价
  const total = () => {
    console.log('我计算了!!!');
    return list.reduce((prev, current) => {
      return prev + current.price;
    }, 0);
  };
  
  <div>总价:{total()}</div>

当我们输入框变化时,total函数会反复执行计算,返回相同的结果,如下图所示:

GIF 2023-4-14 23-21-59.gif

如果这个方法的计算量很大,会造成不必要的性能开销,我们需要的是当list变化时,才进行重新计算,这时useMemo就发挥作用,它在依赖项没有发生变化时,会缓存返回值,避免重复的重新计算,只有当添加或者删除汉堡时也就是依赖项改变时才会重新计算。

  const total = React.useMemo(() => {
    console.log('我计算了!!!');
    return list.reduce((prev, current) => {
      return prev + current.price;
    }, 0);
  }, [list]);

2222222222.gif

完整代码

import React from 'react';
import 'antd/dist/antd.css';
import './index.css';
import { Input, Button, Space } from 'antd';

const App: React.FC = () => {
  console.log('App Render');
  const [value, setValue] = React.useState('');

  const [list, setList] = React.useState([
    {
      name: '鸡腿堡',
      id: 0,
      price: 10,
    },
    {
      name: '牛肉堡',
      id: 1,
      price: 20,
    },
  ]);

  const handleChange = (e) => {
    const {
      target: { value },
    } = e;
    setValue(value);
  };

  const handleAdd = () => {
    setList([...list, { id: Number(new Date()), name: value, price: 50 }]);
  };

  const handleRemove = React.useCallback(
    (id) => {
      setList([...list.filter((f) => f.id !== id)]);
    },
    [list] // 只有当依赖的list变化时,函数才会重新创建
  );

  const total = React.useMemo(() => {
    console.log('我计算了!!!');
    return list.reduce((prev, current) => {
      return prev + current.price;
    }, 0);
  }, [list]);

  return (
    <div>
      <Space>
        <Input value={value} onChange={handleChange} placeholder="请输入" />
        <Button type="primary" onClick={handleAdd}>
          添加
        </Button>
      </Space>
      <div>总价:{total}</div>
      <List list={list} remove={handleRemove} />
    </div>
  );
};

const List = React.memo((props) => {
  console.log('List Render');
  const { list, remove } = props;
  return list.map((item) => (
    <p style={{ marginTop: 10, padding: 10, background: 'pink' }} key={item.id}>
      <Space>
        {item.name}
        <Button type="link" onClick={() => remove(item.id)}>
          删除
        </Button>
      </Space>
    </p>
  ));
});

export default App;

四、总结

  • memo:用于包裹组件,浅层比较其props是否有变化而决定改组件是否需要重新渲染,用于优化性能。
  • useCallback:用于缓存函数,避免函数的重复创建,提高组件的性能。
  • useMemo:用于缓存计算结果,优化组件的性能。