深入学习React18之useCallback源码解析,一篇文章包你看懂

362 阅读4分钟

hello 大家好

如果看过我之前的 useMemo源码解析 这篇文章后 那么你接下来看useCallback源码解析 就会简单很多 废话不多说 上才艺! useCallback 是 React 提供的一个 Hook,用于返回一个 memoized(记忆化)版本的回调函数。其主要目的是优化性能,避免不必要的渲染或重新计算。

解释

当组件重新渲染时,函数组件内部的所有东西都会重新运行。这意味着每次渲染都会创建一个新的回调函数。在大多数情况下,这没什么问题,因为函数创建的成本相对较低。但在某些场景中,重新创建函数可能导致性能问题或不必要的行为,如:

  1. 如果一个回调函数作为 prop 传递给了一个子组件,子组件可能会因为接收到新的回调函数 prop 而进行不必要的重新渲染。
  2. 如果回调函数用于事件处理并且频繁地被重新创建,可能会对性能产生微小的影响。

在这些情况下,useCallback 可以帮助你确保回调函数的引用在多次渲染之间保持不变,除非它依赖的值发生了变化。

用法

useCallback 接受两个参数:

  1. 回调函数
  2. 依赖项数组

返回值是 memoized 的回调函数。

示例

假设你有一个组件,它接受一个 onPress 回调,并且你想确保这个回调只在某个特定的依赖项改变时才被重新创建:

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

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

  const handlePress = useCallback(() => {
    console.log("Button was pressed!");
    setCount(count + 1);
  }, [count]); // 依赖于 `count` 变量

  return (
    <div>
      <button onClick={handlePress}>Press me</button>
      <p>Button pressed {count} times.</p>
    </div>
  );
}

在上述示例中,handlePress 回调函数只有在 count 发生变化时才会被重新创建。如果有其他状态或 props 发生变化并导致 MyComponent 重新渲染,handlePress 的引用仍然保持不变。

注意

  • 不要在 useCallback 的依赖数组中遗漏任何值。如果回调使用了组件的某个 state 或 prop,确保将它包含在依赖数组中。
  • 在许多情况下,你可能并不需要 useCallback。只有当你确实遇到性能问题或不必要的渲染时,才考虑使用它。过度优化可能会使代码更难理解并可能引入错误。

以上案例将useCallback基本功能讲解完毕,下面我们进行useCallback源码分析

源码在packages/react-reconciler/src/ReactFiberHooks.js 中可以找到

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 获取hook状态函数
  const hook = updateWorkInProgressHook();
  // 确定下一个依赖项数组
  const nextDeps = deps === undefined ? null : deps;
  // 获取前一个状态的回调和依赖
  // prevState是一个数组,其中prevState[0]是之前的回调,而prevState[1]是之前的依赖数组
  const prevState = hook.memoizedState;
  if (nextDeps !== null) {
    // 获取依赖数组
    const prevDeps: Array<mixed> | null = prevState[1];
    //比较依赖 类似于Object.is();
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      // 如果新的和旧的依赖项数组相同,则直接返回先前的回调函数,因为没有必要更新它
      return prevState[0];
    }
  }
  //如果新的依赖项和旧的不相等或 nextDeps 为 null,则将新的回调函数和依赖项数组保存到 hook 的状态中。
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

总结

  1. 该函数首先通过调用 updateWorkInProgressHook() 函数来获取当前的 hook 状态。
  2. 确定下一个依赖项数组(nextDeps);如果 depsundefined,则设其为 null
  3. hookmemoizedState 中获取先前状态的回调和依赖项。这个状态是一个数组,其中第一个元素是之前的回调,第二个元素是之前的依赖数组。
  4. 如果 nextDeps 不为 null,则:
    • 从先前状态中获取旧的依赖数组。
    • 使用 areHookInputsEqual 函数比较新旧依赖项。这个函数的功能似乎类似于 Object.is(),用于深度比较。
    • 如果新的依赖项数组与旧的相同,那么就直接返回先前的回调函数,因为没有必要更新它。
  5. 如果新的依赖项数组与旧的不相同,或者 nextDepsnull,则将新的回调函数和依赖项数组保存到 hook 的状态 (memoizedState) 中。
  6. 最后返回新的回调函数。