在 React 开发中,性能优化一直是一个非常重要的话题。其中,防止不必要的渲染和函数对象的重复创建也是开发者需要关注的方面。而 useCallback 就是 React 提供的一个 Hook 函数,用来缓存回调函数,避免重复定义和重新渲染。本篇博客将从以下几个方面浅谈 useCallback 函数。

什么是 useCallback

useCallback 是 React 提供的一个 Hook 函数,用于缓存需要在组件中多次调用的回调函数。它接受两个参数:回调函数和依赖数组。当依赖数组中的任何一个值发生变化时,useCallback 将会返回一个新的回调函数,否则将会返回之前缓存的回调函数。这样可以避免在每次渲染时都重新生成回调函数,从而提高组件的性能。

语法

使用 useCallback 非常简单,只需要传入一个回调函数和一个依赖数组即可。以下是一个示例:

import { useCallback } from 'react';

function MyComponent(props) {
  const handleClick = useCallback(() => {
    console.log('Button clicked!');
  }, []);

  return (
    <button onClick={handleClick}>Click me</button>
  );
}

在上述代码中,我们使用 useCallback 缓存了一个回调函数 handleClick,并将其传递给了按钮的点击事件处理函数。由于依赖数组为空数组,因此 handleClick 变量将会指向同一个回调函数,从而避免了在组件重新渲染时重新创建函数对象。

另外,如果回调函数中依赖了组件状态或属性,那么每次状态或属性改变时 useCallback 都会返回一个新的回调函数:

import { useState, useCallback } from 'react';

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

  const handleClick = useCallback(() => {
    console.log(`Clicked ${count} times`);
  }, [count]);

  return (
    <>
      <div>Count: {count}</div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={handleClick}>Click me</button>
    </>
  );
}

在上述代码中,handleClick 回调函数依赖于 count 状态,因此每次状态改变时都会生成一个新的回调函数。

如何使用 useCallback?

使用前:
// 父组件
import React, { useState } from "react";
import { EgOfUseCallback } from '../../components'

export const Home: React.FC = () => {
  const [count, setCount] = useState(0)
  let [name, setName] = useState('lvxiaobu');

  // 每次点击按钮都会改变状态name,故整个组件重新渲染,故addI函数会重新生成,故子组件即使用了memo包裹,依然重新渲染了
  const addI = () => {
    console.log('走进了addI函数')
  }
  const modifyName = () => {
    setName(name + '6')
  }

  return (
    <>
      <EgOfUseCallback addItem={addI} count={count} />
      现在的名字: {name}  
      <button onClick={modifyName}> 点击修改名字 </button>
    </>
  )
}

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

interface EgOfUseCallbackProps {
  count: number;
  addItem: () => void;
}

export const EgOfUseCallback: React.FC<EgOfUseCallbackProps> = memo(({ addItem, count }) => {
  console.log('子组件render')

  return (
    <div>
      <h3>{count}</h3>
      <button onClick={addItem}>子组件的按钮</button>
    </div>
  )
})

在父组件中,每次点击按钮都会改变状态name,故整个组件重新渲染,故addI函数会重新创建,并且返回栈中新的内存地址,故子组件即使用了memo包裹,依然重新渲染了

使用后:
// 父组件
import React, { useCallback, useState } from "react";
import { EgOfUseCallback } from '../../components'

export const Home: React.FC = () => {
  const [count, setCount] = useState(0)
  let [name, setName] = useState('lvxiaobu');

  // 每次点击按钮都会改变状态name,故整个组件重新渲染,就算使用了useCallback函数,addI函数依然会重新创建,但此时useCallback会返回旧的内存地址,配合memo的浅比较,memo会认为props没有变化,故子组件不会重新渲染
  // 每次点击子组件的按钮时,调用addI函数,更新了count,count有变化,addI函数会重新创建并且useCallback会返回新的内存地址,故addI函数被更新了,(count也更新了,)所以子组件会重新渲染
  const addI = useCallback(() => {
    console.log('走进了addI函数')
    setCount(count + 1)
  }, [count])
  const modifyName = () => {
    setName(name + '6')
  }

  return (
    <>
      <EgOfUseCallback addItem={addI} count={count} />
      现在的名字: {name}  
      <button onClick={modifyName}> 点击修改名字 </button>
    </>
  )
}

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

interface EgOfUseCallbackProps {
  count: number;
  addItem: () => void;
}

export const EgOfUseCallback: React.FC<EgOfUseCallbackProps> = memo(({ addItem, count }) => {
  console.log('子组件render')

  return (
    <div>
      <h3>{count}</h3>
      <button onClick={addItem}>子组件的按钮</button>
    </div>
  )
})

在父组件中,每次点击按钮都会改变状态name,故整个组件重新渲染,就算使用了useCallback函数,addI函数依然会重新创建,但此时useCallback会返回旧的内存地址,配合memo的浅比较,memo会认为props没有变化,故子组件不会重新渲染。
同时,每次点击子组件的按钮时,调用addI函数,更新了count,count有变化,addI函数会重新创建并且useCallback会返回新的内存地址,故addI函数被更新了,(count也更新了,)所以子组件会重新渲染
误区:理所当然地认为当Com组件重新渲染的时候,只有没有使用useCallBack的函数会被重新构建,而使用了useCallBack的函数不会被重新构建。

实际上,被useCallBack包裹了的函数也会被重新构建并当成useCallBack函数的实参传入。

useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址。不论是否使用useCallBack都无法阻止组件render时函数的重新创建!!

每一个被useCallBack的函数都将被加入useCallBack内部的管理队列。而当我们大量使用useCallBack的时候,管理队列中的函数会非常之多,任何一个使用了useCallBack的组件重新渲染的时候都需要去便利useCallBack内部所有被管理的函数找到需要校验依赖是否改变的函数并进行校验。

在以上这个过程中,寻找指定函数需要性能,校验也需要性能。所以,滥用useCallBack不但不能阻止函数重新构建还会增加“寻找指定函数和校验依赖是否改变”这两个功能,为项目增添不必要的负担。

使用 useCallback 的注意事项

虽然 useCallback 是一个非常有用的 Hook 函数,但在使用它时需要注意以下几个细节:

  • 不要滥用 useCallback:只有当你确信某个回调函数需要被缓存时才使用 useCallback,否则它可能会导致不必要的性能问题。
  • 不要在依赖数组中使用函数对象:由于函数对象无法进行浅比较,因此在依赖数组中使用函数对象通常是没有意义的。如果需要在依赖数组中包含函数对象,可以通过将函数对象转换为字符串或使用 useRef 来避免重复创建函数对象。
  • 将 useCallback 提取到顶层:将回调函数定义在组件外部,并将其作为 props 传递给子组件,可以避免在每次父组件重新渲染时都返回新函数对象的新内存地址。

总结

useCallback 是 React 中一个非常有用的 Hook 函数,可以帮助我们缓存需要在组件中多次调用的回调函数。通过合理使用 useCallback,可以减少不必要的返回新函数对象的新内存地址和重新渲染,从而提高组件的性能。然而,在使用 useCallback 时需要注意一些细节,避免出现不必要的问题和性能消耗。总的来说,合理使用 useCallback 可以提高组件性能,并使代码更加清晰易懂。同时,在进行性能优化时,我们也需要综合考虑其他方面,如组件渲染次数、数据的传递方式等,以实现更好的性能表现。

useMemo和useCallback的区别及使用场景

  • useMemo 缓存的结果是回调函数中return回来的值,主要用于缓存计算结果的值,应用场景如需要计算的状态
  • useCallback 缓存的结果是函数,主要用于缓存函数,应用场景如需要缓存的函数,因为函数式组件每次任何一个state发生变化,会触发整个组件更新,一些函数是没有必要更新的,此时就应该缓存起来,提高性能,减少对资源的浪费;另外还需要注意的是,useCallback应该和React.memo配套使用,缺了一个都可能导致性能不升反而下降。
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐