精读React hook(八):我们为什么需要useCallback

《精读useMemo的文章》里,我们知道useMemo也可以缓存函数,但React官方更推荐使用useCallback来缓存函数。本文就来探讨一下useCallback的用法以及我们为什么需要useCallback

什么是 useCallback

useCallback是对useMemo的特化,它可以返回一个缓存版本的函数,只有当它的依赖项改变时,函数才会被重新创建。这意味着如果依赖没有改变,函数引用保持不变,从而避免了因函数引用改变导致的不必要的重新渲染。

useCallback背后的原理是利用闭包和React的调度机制来存储并在必要时重建函数。与直接在组件内创建函数相比,使用useCallback需要付出额外的开销,因为它涉及到存储和检索函数的机制。因此,除非在特定的性能敏感场景中(例如大型列表渲染、频繁的状态更新、与React.memo一同使用等),否则不建议盲目使用它。

为什么需要 useCallback

想象这个场景:你有一个React.memo化的子组件,该子组件接受一个父组件传递的函数作为 prop。如果父组件重新渲染,而且这个函数是在父组件的函数体内定义的,那么每次父组件渲染时,都会为子组件传递一个新的函数实例。这可能会导致子组件不必要地重新渲染,即使该函数的实际内容没有任何变化。

useCallback的主要目的是解决这样的问题。它确保,除非依赖项发生变化,否则函数实例保持不变。这可以防止因为父组件的非相关渲染而导致的子组件的不必要重新渲染。

当然,useCallback真正的应用场景不仅于此,它还可以用于其他需要稳定引用的场景,例如事件处理器、setTimeout/setInterval的回调、函数用于useEffectuseMemouseCallback等的依赖项、或其他可能因为函数引用改变而导致意外行为的场合。

如何使用 useCallback

useCallback的基本使用如下:

jsx 复制代码
const memoizedCallback = useCallback(
  () => {
    // 函数体
  },
  [dependency1, dependency2, ...] // 依赖数组
);

只有当dependency1dependency2等依赖发生改变时,函数才会重新创建。这对于React.memo化的组件、useEffectuseMemo等钩子的输入特别有用,因为它们都依赖于输入的引用恒定性。

来看个示例:

假设我们有一个TodoList组件,其中有一个TodoItem子组件:

jsx 复制代码
function TodoItem({ todo, onDelete }) {
  console.log("TodoItem render:", todo.id);
  return (
    <div>
      {todo.text}
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
}

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Learn React" },
    { id: 2, text: "Learn useCallback" }
  ]);

  const handleDelete = id => {
    setTodos(todos => todos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onDelete={handleDelete} />
      ))}
    </div>
  );
}

上述代码中,每次TodoList重新渲染时,handleDelete都会被重新创建,导致TodoItem也重新渲染。为了优化这一点,我们可以使用useCallback

jsx 复制代码
const handleDelete = useCallback(id => {
  setTodos(todos => todos.filter(todo => todo.id !== id));
}, []);

在这种情况下,handleDelete只会在组件首次渲染时被创建一次。

useMemo 和 useCallback 的差异

用途与缓存的内容不同:

  • useMemo: 用于缓存复杂函数的计算结果或者构造的值。它返回缓存的结果。
  • useCallback: 用于缓存函数本身,确保函数的引用在依赖没有改变时保持稳定。

底层关联:

  • 从本质上说,useCallback(fn, deps)就是useMemo(() => fn, deps)的语法糖:
jsx 复制代码
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}

这里有一个用户评论系统示例,CommentsPage组件可显示文章的评论并允许用户提交新评论:

jsx 复制代码
import React, { useMemo, useCallback } from 'react';

function CommentsPage({ articleId, user }) {
  // 假设 fetchComments 是一个获取评论数据的函数
  const comments = fetchComments('/comments/' + articleId);

  // 对评论数据进行排序
  const sortedComments = useMemo(() => {
    return comments.sort((a, b) => new Date(b.date) - new Date(a.date));
  }, [comments]);

  // 处理新评论的提交
  const handleCommentSubmit = useCallback((commentText) => {
    post('/comments/' + articleId, {
      author: user,
      text: commentText
    });
  }, [articleId, user]);

  return (
    <div>
      <CommentList comments={sortedComments} />
      <CommentForm onSubmit={handleCommentSubmit} />
    </div>
  );
}

在这个示例中,useMemouseCallback用途如下:

  • useMemo用途:sortedComments通过对comments数据按日期进行排序得到。我们不希望每次CommentsPage重新渲染时都重新排序评论,除非comments发生变化。因此,我们使用useMemo来缓存排序结果。
  • useCallback用途:对于handleCommentSubmit函数,我们不希望它在articleIduser保持不变的情况下有一个新的引用。因此,我们使用useCallback来确保函数引用的稳定性。

什么时候使用useCallback

使用useCallback不意味着总是会带来性能提升,这是对useCallback使用场景的简单总结:

使用useCallback

  1. 子组件的性能优化 :当你将函数作为 prop 传递给已经通过React.memo进行优化的子组件时,使用useCallback可以确保子组件不会因为父组件中的函数重建而进行不必要的重新渲染。
  2. Hook 依赖 :如果你正在传递的函数会被用作其他 Hook(例如useEffect)的依赖时,使用useCallback可确保函数的稳定性,从而避免不必要的副作用的执行。
  3. 复杂计算与频繁的重新渲染 :在应用涉及很多细粒度的交互,如绘图应用或其它需要大量操作和反馈的场景,使用useCallback可以避免因频繁的渲染而导致的性能问题。

避免使用useCallback

  1. 过度优化 :在大部分情况下,函数组件的重新渲染并不会带来明显的性能问题,过度使用useCallback可能会使代码变得复杂且难以维护。
  2. 简单组件 :对于没有经过React.memo优化的子组件或者那些不会因为 prop 变化而重新渲染的组件,使用useCallback是不必要的。
  3. 使代码复杂化 :如果引入useCallback仅仅是为了"可能会"有性能提升,而实际上并没有明确的证据表明确实有性能问题,这可能会降低代码的可读性和可维护性。
  4. 不涉及其它 Hooks 的函数 :如果一个函数并不被用作其他 Hooks 的依赖,并且也不被传递给任何子组件,那么没有理由使用useCallback

除此之外,我们还要注意针对useCallback的依赖项设计,我们需要警惕非必要依赖的混入,造成useCallback的效果大打折扣。例如这个非常典型的案例:

jsx 复制代码
function TodoList() {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState("");

  const handleInputChange = (event) => {
    setInputValue(event.target.value);
  };

  const handleAddTodo = useCallback((text) => {
    const newTodo = { id: Date.now(), text };
    setTodos((prevTodos) => [...prevTodos, newTodo]);
  }, [todos]);  // 这里是问题所在,todos的依赖导致这个useCallback几乎失去了其作用

  return (
    <div>
      <input value={inputValue} onChange={handleInputChange} />
      <button onClick={() => handleAddTodo(inputValue)}>Add Todo</button>
      <ul>
        {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
      </ul>
    </div>
  );
}

在上面的示例中,每当todos改变,handleAddTodo都会重新创建,尽管我们使用了useCallback。这实际上并没有给我们带来预期的性能优化。正确的做法是利用setTodos的函数式更新,这样我们就可以去掉todos依赖:

jsx 复制代码
const handleAddTodo = useCallback((text) => {
  const newTodo = { id: Date.now(), text };
  setTodos((prevTodos) => [...prevTodos, newTodo]);
}, []);  // 注意这里的空依赖数组

结语

useCallback的思想和useMemo如出一辙,因为《精读useMemo的文章》🌍演示站里已经提供了对照示例,所以本文只对useCallback进行解读分析,没有单独提供线上示例。如需验证useCallback缓存的效果,大家可以自己敲一敲示例代码来验证。

系列文章列表

精读React hook(一):useState 的几个基础用法和进阶技巧

精读React hook(二):React状态管理的强大工具------useReducer

精读React hook(三):useContext从基础应用到性能优化

精读React hook(四):useRef的多维用途

精读React hook(五):useEffect使用细节知多少?

精读React hook(六):useLayoutEffect解决了什么问题?

精读React hook(七):用useMemo来减少性能开销

精读React hook(八):我们为什么需要useCallback

未完待续......

相关推荐
前端大卫7 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘22 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare23 分钟前
浅浅看一下设计模式
前端
Lee川27 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端