理解React的useCallback钩子🪝基础知识

1,351 阅读3分钟

前言

本篇文章尝试解释useCallback的基本用法。 在了解useCallback之前,你需要知道JS是怎么处理函数相等性检查。

function fn() {
    return (i)=>i;
}

const f1 = fn();
const f2 = fn();

//返回false
console.log(f1 === f2);

什么是useCallBack

useCallback 是 React 中的一个钩子函数,它主要用来缓存回调函数。

useCallback 接受两个参数:第一个参数是回调函数,第二个参数是依赖数组。当依赖数组中的任意一个值发生变化时,useCallback 会重新创建一个新的回调函数。如果依赖数组为空,则回调函数永远不会被更新。

Demo展示代码链接🔗: codesandbox.io/s/awesome-b…

展示的页面如下: image.png

没有使用useCallback

注意:下面的例子仅仅帮助理解useCallback的用法,并不是使用useCallback的正确场景

首先 我们先写一个不使用useCallback的组件。

//App.jsx 作为父组件
import ChildComponent from "./ChildComponent";
import { useState, useCallback } from "react";

export default function App() {
    const [logic, setLogic] = useState(false);
    const [count, setCount] = useState(0);

    const handleClickCount = () => {
        setCount(count + 1);
    };
    //每次App重新渲染handleCallback都是不同的函数
    const handleCallback() = () => {
        setLogic((pre) => !pre);
    }

    return (
        <div>
            <p>数值: {count}</p>
            <p>布尔值: {logic.toString()}</p>
            <button onClick={handleClickCount}>增加数值</button>
            <ChildComponent callback={handleCallback} />
        </div>
        );
    }

然后我们再来看看 子组件 ChildComponent里面是什么样子的。

//子组件
import { useEffect, memo } from "react";

export default function ChildComponent({ callback }) {
    //使用useEffect来监听callback是否发生变化, ”child“ 会被打印出来
    useEffect(() => {
        console.log("in child useEffect");
    }, [callback]);
    //这里我们观察子组件是否再次渲染,"render child" 也会被打印出来
    console.log("render child");
    
    return <button onClick={callback}>改变布尔值</button>;
}

操作观察

如果我们点击增加数值按钮的时候。”render child“ 和 ”child“ 都会被打印出来。

  1. ”in child useEffect" 被打印,说明ChildComponent props 接收到的callback每次被重新创建。
  2. "render child“ 被打印说明每次点击增加数值按钮的时候,ChildComponent被重新渲染。

使用useCallback来缓存handleCallback可以防止handleCallback每次被重新创建,也就是解决第1条。

修改代码如下

给App.jsx里面加上一个新的函数handleCallbackWithUseCallback。

export default function App() {
    const [logic, setLogic] = useState(false);
    const [count, setCount] = useState(0);

    const handleClickCount = () => {
        setCount(count + 1);
    };
    const handleCallback = ()=> {
        setLogic((pre) => !pre);
    }
    //增加的部分
    const handleCallbackWithUseCallback = useCallback(handleCallback, []);

    return (
        <div>
            <p>数值: {count}</p>
            <p>布尔值: {logic.toString()}</p>
            <button onClick={handleClickCount}>增加数值</button>
            {/* 修改部分 */}
            <ChildComponent callback={handleCallbackWithUseCallback} />
        </div>
    );
}

操作观察

  1. ”in child useEffect" 没有被打印出来
  2. "render child“ 仍然被打印。

useCallback只会缓存回调函数handleCallback。点击增加数值的按钮的时候,父组件中的count变量发生了改变,App组件重新渲染,ChildComponent组件也就随着被渲染了。可是 ChildComponent组件和App的count变量并没有关系,count的改变,我们不希望引发ChildComponent组件的重新渲染。如果要解决组件重新渲染的问题的话,单用useCallback是没有用的,还需要加上React.memo。

useCallback和React.memo同时使用代码。

我们可以新建一个新的文件MemoChildComponent

import react from "react";

import ChildComponent from "./ChildComponent";

export default react.memo(ChildComponent);

把这个MemoChildComponent引入到App中, 并配合useCallback使用。

export default function App() {
    const [logic, setLogic] = useState(false);
    const [count, setCount] = useState(0);

    const handleClickCount = () => {
        setCount(count + 1);
    };
    const handleCallback = ()=> {
        setLogic((pre) => !pre);
    }
    const handleCallbackWithUseCallback = useCallback(() => {
        setLogic((pre) => !pre);
    }, []);

    return (
        <div>
            <p>数值: {count}</p>
            <p>布尔值: {logic.toString()}</p>
            <button onClick={handleClickCount}>增加数值</button>
            <MemoChildComponent callback={handleCallbackWithUseCallback} />
        </div>
    );
}

操作观察

这个时候 如果我们点击增加数值的按钮的时候。我们发现 ”render child“ 和 ”in child useEffect“都不会被打印。Count的变化对MemoChildComponent没有影响。

错误使用useCallback例子

虽然useCallback是一种优化的手段。但是这种优化,也会产生调用useCallback所用的资源消耗,同时加上useCallback也会增加代码的复杂度。对于简单的组件来说,滥用useCallback不仅不会优化,反而会适得其反的。本文中使用的例子也是一个不正确的应用场景,但是可以方便对useCallback使用的理解。

错误例子

import { useCallback } from 'react';

function Parent() {
    const handleClick = useCallback(() => {
        console.log('clicked');
    }, []);

    return <Child onClick={handleClick} />;
}

function Child ({ onClick }) {
    return <button onClick={onClick}>按钮</button>;
}

总结

  • useCallback在它的依赖不变的状况下,总是返回相同的函数
  • useCallback可以和React.memo 结合在一起使用来避免不必要的渲染。