React-Hooks 初识 (六): ReactHooks性能优化手段--> useMemo、useCallback 的基本用法

1,389 阅读6分钟

  大家好,我是张添财。上篇更新了memo的用法,但是在文章结尾处我列举的demo中虽然子组件用memo包裹了,但是父组件渲染仍造成了子组件的rerender。具体原因也给大家分析了,有兴趣的看下我的这篇更文《React性能优化手段Memo防止子组件不必要reRender》。今天就给小伙伴们填填坑,掰扯掰扯useMemo、useCallback是怎么用的。(一定要注意一点,useMemo、useCallback做性能优化时子组件要用memo包裹,没有这个前提,useMemo、useCallback优化是没用的!)

一、useMemo:解决父组件传给子组件的参数是复杂数据类型,子组件仍会渲染的问题

1、使用场景:

  假设以下场景,父组件在调用子组件时传递 info 对象属性,点击父组件的点击增加按钮时,发现控制台会打印出子组件被渲染的信息。

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

// 子组件
const ChildComp = (props:{info:{name, age}}) => {
  console.log('ChildComp...',name,age);
  return (<div>ChildComp...</div>);

};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  const info = { name, age };

  return (
    <div className="App">
      <div>hello world {count}</div>
      <button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
      <MemoChildComp info={info}/>
    </div>
  );
};

export default Parent;

分析原因:

  点击父组件按钮,触发父组件重新渲染;父组件渲染,const info = { name, age } 一行会重新生成一个新对象,导致传递给子组件的 props 变化,进而导致子组件重新渲染。

解决方法:

  使用 useMemo 将对象属性包一层。useMemo 有两个参数:

  • 第一个参数是个函数,返回的对象指向同一个引用,不会创建新对象;
  • 第二个参数是个数组,只有数组中的变量改变时,第一个参数的函数才会返回一个新的对象。   下面请看改进后的代码:
import React, { memo, useMemo, useState } from 'react';

// 子组件
const ChildComp = (info:{info:{name: string, age: number}}) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  
  // 使用 useMemo 将对象属性包一层
  const info = useMemo(() => ({ name, age }), [name, age]);

  return (
    <div className="App">
      <div>hello world {count}</div>
      <button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
      <MemoChildComp info={info}/>
    </div>
  );
};

export default Parent;

2、解释useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo返回一个被记忆的值 !!!。

  注意传入 useMemo的依赖项,这样的话useMemo它仅会在某个依赖项改变时才重新计算 memoized 值,这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

二、useCallback:包裹函数,依赖改变时返回新的函数

1、用法:

  useCallback的第一个参数是函数体,第二个参数是依赖项,只有依赖项中的变量改变时,才会返回一个新的函数

 const myFunction=( ()=>{
           函数体...
 }, [])

demo案例:

  紧接着上面useMemo的例子,假设需要传给子组件一个函数,如下所示,当点击父组件按钮时,发现控制台会打印出子组件被渲染的信息,说明子组件又被重新渲染了。

import React, { memo, useMemo, useState } from 'react';

// 子组件
const ChildComp = (props) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  const info = useMemo(() => ({ name, age }), [name, age]);
  const changeName = () => {
    console.log('输出名称...');
  };

  return (
    <div className="App">
      <div>hello world {count}</div>
      <button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
      <MemoChildComp info={info} changeName={changeName}/>
    </div>
  );
};

export default Parent;

分析原因:

  点击父组件按钮,改变了父组件中 count 变量值(父组件的 state 值),进而导致父组件重新渲染;父组件重新渲染时,会重新创建 changeName 函数,即传给子组件的 changeName 属性发生了变化,子组件props发生变化从而导致子组件渲染;

解决方法:

  修改父组件的 changeName 方法,用 useCallback 钩子函数包裹一层, useCallback 参数与 useMemo 类似

  使用 useCallback 将函数包一层,useCallback 有两个参数:

  • 第一个参数是个函数,返回的函数指向同一个引用,不会创建新函数;
  • 第二个参数是个数组,第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,就会重新返回一个新的记忆函数提供给后面进行渲染。如果是一个空数组则是无论什么情况下该函数都不会发生改变
import React, { memo, useCallback, useMemo, useState } from 'react';

// 子组件
const ChildComp = (props) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  const info = useMemo(() => ({ name, age }), [name, age]);
  const changeName = useCallback(() => {
    console.log('输出名称...');
  }, []);

  return (
    <div className="App">
      <div>hello world {count}</div>
      <button onClick={() => { setCount(count => count + 1); }}>点击增加</button>
      <MemoChildComp info={info} changeName={changeName}/>
    </div>
  );
};

export default Parent;

优化后分析: 用useCallback包裹后,父组件render时,包裹后的函数因为依赖项不变所以还是用记忆函数,则MemoChildComp组件中changeName并没有发生改变,那个子组件props也没有改变,也就不会进行rerender。

补充: useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。

useCallback(fn, inputs)    就相当于   useMemo(() => fn, inputs).

前面使用 useCallback 的例子可以使用 useMemo 进行改写:

这是通过useMemo进行改写上面useCallback这个例子。
  const changeName = useMemo(() => () => {
    console.log('输出名称...');
  }, []); // 空数组代表无论什么情况下该函数都不会发生改变
 

  唯一的区别是:useCallback不会执行第一个参数函数,而是将它返回给你,而useMemo会执行第一个函数并且将函数执行结果返回给你

总结: useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值

写在最后:

  最后再给大家总结下各钩子的使用场景。

  • useState:涉及到React状态的存取改变时进行使用;state改变时会触发render,需要注意的是只能用setState来改变状态值。
  • useEffect: 处理副作用的钩子,场景上可以模拟类写法中的生命周期来进行使用。
  • useRef: 获取DOM元素或保存变量,可以绕过capture Value特性,也就是说无论在哪里取值,useRef保存的变量都是最新的。
  • useImperativeHandle: 父组件调用子组件的属性,要结合forwardRef使用。
  • memo、useMemo、useCallback:三者结合使用,可以有效的减少项目里组件的rerender,是React的一种性能优化手段。

  到这里我们hooks系列就暂时告一段落了,这篇文章也算是给整个hooks系列收了收尾。其实在实际业务场景中,用这几个钩子就已经绰绰有余了。后续的话可能会写一些偏业务相关的。也欢迎各位小伙伴评论区留言,有其他感兴趣的话题我们也可以共同讨论下。好了,本系列到此结束,各位同好继续加油呐💪