React Hooks大全—useCallback

在本文中,我们将重点介绍一个React常用的内置Hook,即useCallback。useCallback可以让我们缓存函数,避免因为函数引用的变化而导致不必要的子组件重渲染。我们讲解它的基本使用、实现原理、与useMemo的区别、最佳实践等。

基本使用

公众号:Code程序人生,个人网站:https://creatorblog.cn

useCallback是一个React Hook,所以我们只能在函数式组件或者自定义Hook中调用它,不能在循环或者条件语句中调用它。useCallback的基本语法如下:

const cachedFn = useCallback(fn, dependencies);

useCallback接受两个参数,分别是:

  • fn:一个函数,它可以接受任意的参数,返回任意的值。这个函数是我们想要缓存的函数,它的定义不应该依赖于组件的状态或者属性,否则可能会导致缓存失效或者闭包陷阱。
  • dependencies:一个数组,它包含了所有fn中引用的依赖项,例如状态、属性、变量或者函数等。这些依赖项必须是可比较的,即可以用Object.is来判断它们是否相等。如果我们忽略了某些依赖项,或者提供了一个空数组,那么useCallback将无法正确地更新缓存的函数,可能会导致意想不到的结果。

useCallback的返回值是一个函数,它是fn的一个缓存版本。

在组件的初始渲染时,useCallback会返回fn本身。在后续的渲染中,useCallback会根据依赖项的变化来决定是否返回上一次的缓存函数,还是返回当前的fn

如果依赖项没有变化,useCallback会返回上一次的缓存函数,这样可以保证函数的引用不变,从而避免触发子组件的重渲染。如果依赖项有变化,useCallback会返回当前的fn,并将其缓存起来,以备下次使用。

下面是一个简单的例子,演示了useCallback的基本用法:

import React, { useState, useCallback } from "react";

// 一个子组件,接受一个函数作为属性
function Child({ onClick }) {
  console.log("Child rendered");
  return <button onClick={onClick}>Click Me</button>;
}

// 一个父组件,使用useCallback来缓存一个函数
function Parent() {
  const [count, setCount] = useState(0);

  // 使用useCallback来创建一个缓存的函数,该函数会更新count状态
  // 该函数的依赖项是setCount,它是一个稳定的函数,不会随着渲染而变化
  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, [setCount]);

  return (
    <div>
      <p>Count: {count}</p>
      {/* 将缓存的函数传递给子组件 */}
      <Child onClick={handleClick} />
    </div>
  );
}

在这个例子中,父组件使用useState来创建一个count状态,然后使用useCallback来创建一个缓存的函数,该函数会更新count状态。注意,这个函数的依赖项是setCount,它是一个稳定的函数,不会随着渲染而变化,所以我们可以放心地将它作为依赖项。然后,父组件将这个缓存的函数传递给子组件,子组件接受这个函数作为属性,并在按钮上绑定这个函数。

当我们运行这个例子时,我们可以看到,每当我们点击按钮时,count状态会增加,父组件会重新渲染,但是子组件不会重新渲染,因为它接收的函数的引用没有变化。这样,我们就利用useCallback来优化了子组件的性能,避免了不必要的重渲染。

实现原理

useCallback的实现原理其实很简单,它就是利用了闭包和数组来存储和比较函数和依赖项。我们可以用以下的伪代码来模拟useCallback的实现过程:

// 定义一个全局的缓存对象,用来存储函数和依赖项
const cache = {
  fn: null, // 存储函数
  dependencies: [], // 存储依赖项
};

// 定义一个useCallback函数,接受函数和依赖项作为参数,返回一个缓存的函数
function useCallback(fn, dependencies) {
  // 如果缓存对象中没有存储函数,或者依赖项的长度不一致,或者依赖项有变化
  if (
    cache.fn === null ||
    cache.dependencies.length !== dependencies.length ||
    dependencies.some((dep, i) => !Object.is(dep, cache.dependencies[i]))
  ) {
    // 将当前的函数和依赖项存储到缓存对象中
    cache.fn = fn;
    cache.dependencies = dependencies;
  }
  // 返回缓存对象中的函数
  return cache.fn;
}

从上面的伪代码中,我们可以看到,useCallback其实就是通过一个全局的缓存对象来存储和返回函数,同时通过比较依赖项的长度和值来判断是否需要更新缓存对象。当然,这只是一个简化的版本,实际的useCallback可能会更复杂一些,但是基本的思路是一样的。

有些人可能会问,useCallbackuseMemo有什么区别呢?它们不都是用来缓存值的吗?

其实,useCallbackuseMemo的区别主要在于它们缓存的值的类型。useCallback缓存的是函数,而useMemo缓存的是任意的值,包括函数、对象、数组等。

useCallbackuseMemo的用法也有一些不同,useCallback返回的是一个函数,我们可以直接调用它,而useMemo返回的是一个值,我们需要将它赋值给一个变量或者常量。useCallbackuseMemo的语法如下:

// useCallback的语法
const cachedFn = useCallback(fn, dependencies);

// useMemo的语法
const cachedValue = useMemo(() => value, dependencies);

从上面的语法中,我们可以看到,useCallback接受一个函数作为第一个参数,而useMemo接受一个函数的返回值作为第一个参数。

这意味着,useCallback只会在依赖项变化时执行一次函数,而useMemo会在每次渲染时都执行一次函数,只是在依赖项变化时才会更新缓存的值。

因此,useCallback适合用来缓存那些不需要立即执行的函数,而useMemo适合用来缓存那些需要立即执行的值。

下面是一个例子,演示了useCallbackuseMemo的区别:

import React, { useState, useCallback, useMemo } from "react";

// 一个子组件,接受一个函数和一个值作为属性
function Child({ onClick, value }) {
  console.log("Child rendered");
  return (
    <div>
      <p>Value: {value}</p>
      <button onClick={onClick}>Click Me</button>
    </div>
  );
}

// 一个父组件,使用useCallback和useMemo来缓存一个函数和一个值
function Parent() {
  const [count, setCount] = useState(0);

  // 使用useCallback来创建一个缓存的函数,该函数会更新count状态
  // 该函数的依赖项是setCount,它是一个稳定的函数,不会随着渲染而变化
  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, [setCount]);

  // 使用useMemo来创建一个缓存的值,该值是count的平方
  // 该值的依赖项是count,它是一个可变的状态,会随着渲染而变化
  const squared = useMemo(() => {
    console.log("squared computed");
    return count * count;
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      {/* 将缓存的函数和值传递给子组件 */}
      <Child onClick={handleClick} value={squared} />
    </div>
  );
}

在这个例子中,父组件使用useState来创建一个count状态,然后使用useCallback来创建一个缓存的函数,该函数会更新count状态。

注意,这个函数的依赖项是setCount,它是一个稳定的函数,不会随着渲染而变化,所以我们可以放心地将它作为依赖项。

然后,父组件使用useMemo来创建一个缓存的值,该值是count的平方。注意,这个值的依赖项是count,它是一个可变的状态,会随着渲染而变化,所以我们需要将它作为依赖项。然后,父组件将这个缓存的函数和值传递给子组件,子组件接受这个函数和值作为属性,并在按钮上绑定这个函数,以及显示这个值。

当我们运行这个例子时,我们可以看到,每当我们点击按钮时,count状态会增加,父组件会重新渲染,子组件也会重新渲染,因为它接收的值的引用发生了变化。

同时,我们可以看到控制台的输出,每当我们点击按钮时,useMemo会重新计算squared的值,并打印出"squared computed",而useCallback不会重新执行函数,只是返回上一次的缓存函数。这样,我们就可以看出useCallbackuseMemo的区别和联系。

最佳实践

useCallback是一个有用的Hook,但是它也有一些需要注意的地方。在使用useCallback时,我们应该遵循以下的一些最佳实践:

  • 不要滥用useCallback。useCallback并不是一个万能的性能优化工具,它也有一定的开销,例如创建和比较依赖项数组,以及存储和返回缓存函数。如果我们不需要缓存函数,或者缓存函数的收益小于开销,那么使用useCallback反而会降低性能,而不是提高性能。因此,我们应该在必要的时候才使用useCallback,例如当我们需要将函数作为属性传递给子组件,或者当我们需要将函数作为依赖项传递给其他Hook时。
  • 不要忽略依赖项。useCallback的第二个参数是一个非常重要的参数,它决定了缓存函数的有效性和正确性。如果我们忽略了某些依赖项,或者提供了一个空数组,那么useCallback将无法正确地更新缓存函数,可能会导致缓存函数引用了过期的状态或者属性,或者触发了无限循环或者内存泄漏等问题。因此,我们应该尽量完整地提供所有的依赖项,或者使用一些工具,例如eslint-plugin-react-hooks,来帮助我们检查和修复依赖项的问题。
  • 不要在缓存函数中定义状态或者属性。useCallback的第一个参数是一个函数,它可以接受任意的参数,返回任意的值。但是,这个函数的定义不应该依赖于组件的状态或者属性,否则可能会导致缓存失效或者闭包陷阱。例如,下面的代码就是一个错误的用法:
import React, { useState, useCallback } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  // 错误的用法:在缓存函数中定义了一个状态
  const increment = useCallback(() => {
    // 这里的count是一个闭包变量,它只会在初始渲染时被捕获,后续的渲染不会更新它
    // 这会导致increment函数总是返回1,而不是正确的count + 1
    const [count, setCount] = useState(0);
    return count + 1;
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(increment)}>Increment</button>
    </div>
  );
}

在这个例子中,我们在缓存函数中定义了一个状态,然后将这个缓存函数作为setCount的参数。

这是一个错误的用法,因为这个缓存函数只会在初始渲染时被创建,它捕获了当时的count值,后续的渲染不会更新这个值,导致increment函数总是返回1,而不是正确的count + 1

这就是一个典型的闭包陷阱,它会让我们的状态不同步,造成逻辑错误。为了避免这个问题,我们应该将状态或者属性作为缓存函数的参数,而不是在缓存函数中定义它们。例如,下面的代码就是一个正确的用法:

import React, { useState, useCallback } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  // 正确的用法:将状态作为缓存函数的参数
  const increment = useCallback((count) => {
    // 这里的count是一个参数,它会随着setCount的调用而更新,保持同步
    // 这会导致increment函数返回正确的count + 1
    return count + 1;
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((prevCount) => increment(prevCount))}>
        Increment
      </button>
    </div>
  );
}

在这个例子中,我们将count作为缓存函数的参数,然后将这个缓存函数作为setCount的参数。这是一个正确的用法,因为这个缓存函数不会依赖于任何状态或者属性,它只会根据传入的参数来返回一个值,这样就避免了闭包陷阱,保证了状态的同步,实现了逻辑的正确。

总结

useCallback是一个可以让我们缓存函数的Hook,它可以帮助我们优化性能,避免不必要的子组件重渲染。

useCallback的实现原理其实很简单,它就是利用了闭包和数组来存储和比较函数和依赖项。

useCallbackuseMemo的区别主要在于它们缓存的值的类型。useCallback缓存的是函数,而useMemo缓存的是任意的值,包括函数、对象、数组等。

不要滥用useCallback,它也有开销。不要忽略依赖项,它决定了缓存函数的正确性。不要在缓存函数中定义状态或者属性,它会导致闭包陷阱。

你可能感兴趣的:(react,前端,面试,javascript,前端,react.js)