📜  何时使用 useCallback、useMemo 和 useEffect ?

📅  最后修改于: 2022-05-13 01:56:28.379000             🧑  作者: Mango

何时使用 useCallback、useMemo 和 useEffect ?

useCallback、useMemo 和 useEffect 是一种在重新渲染组件之间优化基于 React 的应用程序性能的方法。这些函数提供了基于类的组件的一些特性,例如通过渲染调用保持专用状态以及生命周期函数来控制组件如何查看其生命周期的各个阶段。

要回答何时使用 useCallBack、useMemo 和 useEffect,我们应该知道它们究竟做了什么以及它们有何不同。

  1. useCallback: useCallback 是一个反应钩子,当传递一个函数和一个依赖项列表作为参数时,它会返回一个记忆回调。当组件向其子组件传递回调以防止子组件的渲染时,它非常有用。它仅在其依赖项之一发生更改时更改回调。

  2. useMemo: useMemo 类似于 useCallback 钩子,因为它接受一个函数和一个依赖项列表,但它返回传递函数返回的记忆值。它仅在其依赖项之一发生更改时才重新计算该值。当返回值不会改变时,避免在每次渲染上进行昂贵的计算很有用。

  3. useEffect:一个钩子,帮助我们在渲染所有组件后执行突变、订阅、计时器、日志记录和其他副作用。 useEffect 接受本质上是命令式的函数和依赖项列表。当其依赖关系发生变化时,它会执行传递的函数。

创建一个反应应用程序来理解所有三个钩子:

  • 第 1 步:使用以下命令创建一个 React 应用程序:

    npx create-react-app usecallbackdemo
  • 第 2 步:创建项目文件夹(即文件夹名称)后使用以下命令移动到该文件夹:

    cd usecallbackdemo

项目结构:它将如下所示。

项目结构

现在让我们了解所有三个钩子的工作原理。

1、usecallback:依赖于引用相等。在 javascript 中,函数是一等公民,这意味着函数是常规对象。因此,即使共享相同代码的两个函数对象也是两个不同的对象。请记住,函数对象在引用上仅与自身相等。

让我们在下面的代码中看到这一点, doubleFactory创建并返回一个函数:

Javascript
function doubleFactory(){
    return (a) => 2*a;
}
  
const double1 = doubleFactory();
const double2 = doubleFactory();
  
double1(8); // gives 16
double2(8); // gives 16
  
double1 === double2;  // false
double1 === double1;  // true


Javascript
function MyComponent(){
  
    // HandleChange is created on every render
    const handleChange = () => {...};
      
    return <> 
        ... 
        ;
}


App.jsx
import React, { useState} from "react"
import List from "./List"
  
function App(){
  
    {/* Initial states */}
    const [input, setInput] = useState(1);
    const [light, setLight] = useState(true);
  
    {/* getItems() returns a list of number which
    is number+10 and number + 100 */}
    const getItems = () => {
        return [input + 10, input + 100];
    }
  
    {/* Style for changing the theme */}
    const theme = {
        backgroundColor: light ? "White": "grey",
        color: light ? "grey" : "white"
    }
     
    return <>
        {/* set the theme in the parent div */}
        
             setInput(parseInt(event.target.value))} />                {/* on click the button the theme is set to the              opposite mode, light to dark and vice versa*/}                                   
    ; }    export default App;


List.jsx
import React, { useEffect, useState } from "react"
  
function List({ getItems }){
  
    /* Initial state of the items */
    const [items, setItems] = useState([]);
  
    /* This hook sets the value of items if 
       getItems object changes */
    useEffect(() => {
        console.log("Fetching items");
        setItems(getItems());
    }, [getItems]);
  
    /* Maps the items to a list */
    return 
        {items.map(item =>
{item}
)}     
} export default List;


App.jsx
import React, { useCallback, useState} from "react"
import List from "./List"
  
function App(){
  
    {/* Initial states */}
    const [input, setInput] = useState(1);
    const [light, setLight] = useState(true);
  
    {/* useCallback memoizes the getItems() which 
       returns a list of number which is number+10
       and number + 100 */}
    const getItems = useCallback(() => {
        return [input + 10, input + 100];
    }, [input]);
  
    {/* style for changing the theme */}
    const theme = {
        backgroundColor: light ? "White": "grey",
        color: light ? "grey" : "white"
    }
      
  
    return <>
        {/* set the theme in the parent div */}
        
                          setInput(parseInt(event.target.value))             } />                {/* on click the button the theme is set to              the opposite mode, light to dark and vice versa*/}                                   
    ; }    export default App;


MyComponent.jsx
function MyComponent(){
    const [data, setData] = useState(0);
    const number = verySlowFunction(data);
    return 
{number}
; }    function verySlowFunction(input){     ...heavy work done here     return value; }


MyComponent.jsx
function MyComponent(){
    const [data, setData] = useState(0);
    const number = useMemo(() => {
        return verySlowFunction(data)}, [data]);
      
    return 
{number}
; }    function verySlowFunction(input){     ...heavy work done here     return value; }


MyComponent.jsx
function MyComponent() {    
    const [number, setNumber] = useState(0);
    const data = {
        key: value
    };
      
    useEffect(() => {
        console.log('Hello world');
    }, [data]);
}


MyComponent.jsx
function MyComponent(){
      
    const [number, setNumber] = useState(0);
  
    const data = useMemo( () => {
    return {
        key: value
    }}, number);
      
    useEffect(() => {
        console.log('Hello world');
    }, [data]);
}


App.jsx
import React, { useEffect, useState} from "react"
  
  
function App(){
  
    /* Some data */
    const data = {
        Colors: ["red", "green", "yellow"],
        Fruits: ["Apple", "mango", "Banana"]
    }
  
    /* Initial states */
    const [currentChoice, setCurrentChoice] = useState("Colors");
    const [items, setItems] = useState([]);
  
    /* Using useEffect to set the data of currentchoice
       to items and console log the fetching... */
    useEffect(() => {
        setItems(data[currentChoice]);
        console.log("Data is fetched!");
    }, [currentChoice]);
      
    return <>
    
        
        {items.map(item => {return 
{item}
})}     ; }    export default App;


doube1double2将传递给它们的值加倍,并由相同的工厂函数创建。即使它们共享相同的代码,这两个函数也不相等,这里 ( double1 === double2)的计算结果为 false。

何时使用 useCallback:在 React 中,组件通常会在其中创建一些回调函数。

Javascript

function MyComponent(){
  
    // HandleChange is created on every render
    const handleChange = () => {...};
      
    return <> 
        ... 
        ;
}

在 MyComponent 的每次渲染中, handleChange函数对象都是不同的。并且有几种情况我们可能希望在多个渲染之间使用相同的函数对象。例如,当它是一些其他钩子的依赖项(useEffect( ..., callbackfunc))或函数对象本身具有我们需要维护的某些内部状态时。在这种情况下,useCallback 钩子就派上用场了。简单来说, useCallback(callBackFun, deps)在依赖值deps在渲染之间没有变化时返回一个记忆回调。 (这里的记忆是指缓存对象以备将来使用)。

让我们看一个使用项目的用例:该应用程序由一个输入字段、一个按钮和一个列表组成。该列表是一个由两个数字组成的组件,第一个是输入加 10,第二个是输入 + 100。按钮将组件从暗模式更改为亮模式,反之亦然。

将有两个组件 App 和 List,App 是我们添加输入字段、按钮和 List 的主要组件。列表组件用于根据输入字段打印项目列表。

应用程序.jsx

import React, { useState} from "react"
import List from "./List"
  
function App(){
  
    {/* Initial states */}
    const [input, setInput] = useState(1);
    const [light, setLight] = useState(true);
  
    {/* getItems() returns a list of number which
    is number+10 and number + 100 */}
    const getItems = () => {
        return [input + 10, input + 100];
    }
  
    {/* Style for changing the theme */}
    const theme = {
        backgroundColor: light ? "White": "grey",
        color: light ? "grey" : "white"
    }
     
    return <>
        {/* set the theme in the parent div */}
        
             setInput(parseInt(event.target.value))} />                {/* on click the button the theme is set to the              opposite mode, light to dark and vice versa*/}                                   
    ; }    export default App;

列表.jsx

import React, { useEffect, useState } from "react"
  
function List({ getItems }){
  
    /* Initial state of the items */
    const [items, setItems] = useState([]);
  
    /* This hook sets the value of items if 
       getItems object changes */
    useEffect(() => {
        console.log("Fetching items");
        setItems(getItems());
    }, [getItems]);
  
    /* Maps the items to a list */
    return 
        {items.map(item =>
{item}
)}     
} export default List;

说明:列表组件获取 getItems函数作为属性。每次getItems函数对象发生变化useEffect都会调用setItems将函数对象返回的列表设置为有状态的变量item,然后我们将这些item映射到一个div列表中。每次在useEffect中使用getItems获取item时,我们打印“Fetching items”以查看获取项目的频率

运行应用程序的步骤:

npm start

输出:

说明:当用户在输入字段中输入数字时,将输出以下内容。从控制台日志可以看出,当app第一次渲染时,会抓取item,并打印出“fetching items”。现在,如果我们输入一些不同的数字,我们会看到再次获取项目。

现在奇怪的是,当我们按下按钮更改主题时,我们看到即使输入字段没有修改,项目仍在被获取。

这种行为背后的原因是,当我们按下按钮时,应用程序组件被重新渲染,因此 App 内部的函数getItems()再次被创建,我们知道两个对象在引用上是不同的。因此,在 List 组件内部,useEffect 挂钩调用 setItems 并在其依赖项发生变化时打印“Fetching items”。

上述问题的解决方法:这里我们可以使用useCallback函数根据输入的数字来记忆getItems()函数。除非输入更改,否则我们不想重新创建该函数,因此,在按下按钮(更改主题)时将不会获取项目。

应用程序.jsx

import React, { useCallback, useState} from "react"
import List from "./List"
  
function App(){
  
    {/* Initial states */}
    const [input, setInput] = useState(1);
    const [light, setLight] = useState(true);
  
    {/* useCallback memoizes the getItems() which 
       returns a list of number which is number+10
       and number + 100 */}
    const getItems = useCallback(() => {
        return [input + 10, input + 100];
    }, [input]);
  
    {/* style for changing the theme */}
    const theme = {
        backgroundColor: light ? "White": "grey",
        color: light ? "grey" : "white"
    }
      
  
    return <>
        {/* set the theme in the parent div */}
        
                          setInput(parseInt(event.target.value))             } />                {/* on click the button the theme is set to              the opposite mode, light to dark and vice versa*/}                                   
    ; }    export default App;

现在我们使用 useCallback 钩子来记忆 getitems函数,该函数接受函数和依赖列表。在我们的例子中,依赖列表只包含输入。

输出:

解释:从输出中可以看出,在渲染应用程序时仅获取一次项目,而在按下按钮更改主题时则不会。无论我们翻转主题多少次,useEffect 都不会调用setItems ,直到输入字段有一个新数字。

2. useMemo: useMemo 钩子在获取一个函数和一个依赖列表后返回一个记忆值。如果依赖关系没有改变,它会返回缓存的值。否则,它将使用传递的函数重新计算值。

何时使用 useMemo:

在两种情况下使用 useMemo 会有所帮助:

  1. 当组件使用使用耗时函数计算的值时。

    我的组件.jsx

    function MyComponent(){
        const [data, setData] = useState(0);
        const number = verySlowFunction(data);
        return 
    {number}
    ; }    function verySlowFunction(input){     ...heavy work done here     return value; }

    这里每次渲染MyComponent时都会调用 slow函数,可能是因为某些状态变量发生了变化或某些其他组件导致了重新渲染。

    解决方案:通过使用 useMemo 钩子来记忆慢速函数的返回值,我们可以避免它可能导致的延迟。

    我的组件.jsx

    function MyComponent(){
        const [data, setData] = useState(0);
        const number = useMemo(() => {
            return verySlowFunction(data)}, [data]);
          
        return 
    {number}
    ; }    function verySlowFunction(input){     ...heavy work done here     return value; }

    这里我们使用 useMemo 挂钩来缓存返回的值,并且依赖列表包含数据状态变量。现在每次渲染组件时,如果数据变量没有改变,我们就可以在不调用 CPU 密集型函数的情况下获得记忆值。因此,它提高了性能。

  2. 现在考虑另一种情况,当我们有一个组件在某些数据更改时执行某些操作,例如,让我们使用钩子 useEffect 来记录某些依赖项更改时的日志。

    我的组件.jsx

    function MyComponent() {    
        const [number, setNumber] = useState(0);
        const data = {
            key: value
        };
          
        useEffect(() => {
            console.log('Hello world');
        }, [data]);
    }
    

    在上面的代码中,每次渲染组件时,都会在控制台上打印“Hello world”,因为存储在前一个渲染中的数据对象在下一个渲染中引用不同,因此 useEffect 挂钩运行console.log函数。在现实世界中 useEffect 可以包含一些我们不想重复的功能,如果它的依赖关系没有改变的话。

    解决方案:我们可以使用 useMemo 钩子来记忆数据对象,这样组件的渲染就不会创建新的数据对象,因此 useEffect 不会调用它的主体。

    我的组件.jsx

    function MyComponent(){
          
        const [number, setNumber] = useState(0);
      
        const data = useMemo( () => {
        return {
            key: value
        }}, number);
          
        useEffect(() => {
            console.log('Hello world');
        }, [data]);
    }
    

    现在,当组件第二次呈现时,如果未修改 number 状态变量,则不会执行 console.log()。

3. useEffect:在react中,一些状态变化的副作用在功能组件中是不允许的。要在渲染完成并且某些状态发生变化后执行任务,我们可以使用 useEffect。这个钩子需要一个要执行的函数和一个依赖列表,改变这将导致钩子主体的执行。

了解其正确使用方法。让我们看一个简单的例子:

示例:考虑一个场景,一旦安装了组件,我们必须从某些 API 获取一些数据。在示例代码中,我们使用具有不同颜色和水果值的数据对象来模拟服务器。我们想根据按下的按钮打印项目列表。因此,我们有两个状态变量currentChoice和通过按下按钮修改的项目。当按下按钮时,它会更改currentChoice并调用 useEffect 的主体,并使用地图打印当前选择的项目。现在,如果我们不使用 useEffect,每次按下按钮时都会从服务器获取数据,即使选择没有改变。在这种情况下,这个钩子可以帮助我们不调用获取逻辑,除非我们的选择发生变化。

应用程序.jsx

import React, { useEffect, useState} from "react"
  
  
function App(){
  
    /* Some data */
    const data = {
        Colors: ["red", "green", "yellow"],
        Fruits: ["Apple", "mango", "Banana"]
    }
  
    /* Initial states */
    const [currentChoice, setCurrentChoice] = useState("Colors");
    const [items, setItems] = useState([]);
  
    /* Using useEffect to set the data of currentchoice
       to items and console log the fetching... */
    useEffect(() => {
        setItems(data[currentChoice]);
        console.log("Data is fetched!");
    }, [currentChoice]);
      
    return <>
    
        
        {items.map(item => {return 
{item}
})}     ; }    export default App;

输出:

说明:当应用程序第一次加载时,数据是从我们的假服务器中获取的。这可以在下图中的控制台中看到。当我们按下 Fruits 按钮时,会再次从服务器获取适当的数据,我们可以看到“Data is fetched”再次打印在控制台中。但是如果我们再次按下 Fruits 按钮,我们就不必再次从服务器获取数据,因为我们的选择状态不会改变。

结论:

因此,当我们想要记忆回调时,应该使用 useCallback 挂钩,并且为了避免昂贵的计算,我们可以使用 useMemo 来记忆函数的结果。 useEffect 用于对某些状态更改产生副作用。要记住的一点是,不应过度使用钩子。