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

在这里插入图片描述

🎯 【专栏:精读React Hooks】我用16篇文章详细解读16个React官方的Hook,每一篇都尽力做到比官方文档更仔细且更易读,同时提供了开源demo作为演示。如果你是新手,可以把这个专栏当作学习材料,如果你有一定经验了,可以把这份专栏当作查缺补漏的资料。
专栏首发地址:J实验室 - React Hooks
专栏演示地址:React Hooks Demo
一起学习交流:「交个朋友

《精读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的基本使用如下:

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

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

来看个示例:

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

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

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

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

useMemo 和 useCallback 的差异

用途与缓存的内容不同:

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

底层关联:

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

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

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的效果大打折扣。例如这个非常典型的案例:

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依赖:

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

结语

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

专栏资源

专栏首发地址:👉 精读React Hooks
专栏演示地址:👉 React Hooks Demos
专栏源码仓库:👉 Github
一起学习交流:👉交个朋友

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BigYe程普

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值