useCallback、useMemo和无限循环

摩森特沃 2021年07月06日 1,275次浏览

问题引出

先看一下这段代码:

export default function App() {
  const value = { name: 1 };

  React.useEffect(() => {
    alert("render");
  }, [value]);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Edit to see some magic happen!</h2>
    </div>
  );
}

在代码中每次App组件渲染,value变量都会被定义一次

那么,请问上面这段代码中,App渲染几次,value被定义几次, alert 会被弹出几次?

答案是:

  1. 第一次初始化以后,没有任何事情能引起它的再次渲染(因为没有父组件、没有状态/props改变),所以只会渲染一次
  2. 因为只渲染一次,value也只会被定义一次
  3. 而useEffect的执行时机,是在组件渲染后,由于只渲染一次,所以useEffect只执行一次,所以alert只弹出一次

再看这段代码:

import "./styles.css";
import React from "react";

export default function App() {
  const [count, setCount] = React.useState(0); // 加了这一行
  const value = { name: 1 };

  React.useEffect(() => {
    setCount(Math.random()); // 加了这一行
    ("render");
  }, [value]);
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Edit to see some magic happen!</h2>
    </div>
  );
}

这段代码比上一段多了两句,我已经在注释中标出来了,请问现在 App渲染几次,value被定义几次, alert 会被弹出几次呢?

答案是:无限循环,全都无限次

这里循环的原因是:组件渲染 → useEffect执行 → setCount触发循环 → 组件渲染 → useEffect执行 → setCount触发循环...

绝大多数的无限循环的情况,都是这段代码的缩影

useCallback和useMemo

针对上面的问题,React 官方提供的方法,就是使用 useMemo:

import "./styles.css";
import React from "react";

export default function App() {
  const [count, setCount] = React.useState(0); 
  const value = React.useMemo(() => {
    return { name: 1 };
  }, []);

  React.useEffect(() => {
    setCount(Math.random()); 
    alert("render");
  }, [value]);
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Edit to see some magic happen!</h2>
    </div>
  );
}

useMemo 的意思就是:不要每次渲染都重新定义,而是需要重新定义的时候再重新定义(第二个参数,依赖列表)。这里的依赖列表是空的,是因为useMemo里的回调函数确实没用到啥变量,如果有变量的话IDE也会提醒加上依赖。

这就是使用useMemo的原理,useMemo适用于所有类型的值,加入这个值恰好是函数,那么用useCallback也可以。也就是说,useCallback是一种特殊的useMemo。

使用总结

如果定义了一个变量,满足下面的条件就最好用useMemo和useCallback给包裹住:

  1. 它不是状态,也就是说,不是用useState定义的(redux中的状态实际上也是用useState定义的)
  2. 它不是基本类型
  3. 它会被放在useEffect的依赖列表里 || 自定义hook的返回值

说一下第3条,中间的两个竖线是 或,也就是两者满足其一第3条就成立。自定义hook的返回值也成立是因为,你不知道自定义hook的返回值将会被用在哪里,它可能会被用在依赖也可能不会,所以干脆都加上;而像上面那个在组件中定义的value,你就可以见机行事了

上面例子中的value变量就是一个经典的满足这三个条件的例子,只要遇到这个场景就使用useMemo和useCallback,就不会有无限循环的问题。

当然,更简单粗暴的是,在**定义(不是使用!)**所有"非状态"的变量的时候都用useMemo和useCallback包裹中,也不会有无限循环的问题。但是没必要这么做,代码也不好看。