这是一篇关于 React Hooks 的技术文章翻译,原文地址: https://dev.to/michael_osas/understanding-react-hooks-how-to-use-useref-usememo-and-usecallback-for-more-efficient-code-3ceh,翻译不当之处请指正。

译者评价

文章主要介绍了 useRef、useMemo 和 useCallback 3 个 React Hook,读者可以通过此文了解 3 种 Hook 的使用方式、场景,但文章也存在一些缺点:

  1. 内容重复严重,如 useRef 的作用在文章前中后段中均有描述
  2. useCallback 的示例举例不当,容易造成读者的困惑

文章的内容只适合 3 个 React Hook 的基础泛读,因为示例足够简单,所以读者读起来并不会太大的压力。

正文开始

在不编写和使用类组件的情况下,React Hooks 是允许开发者在函数组件中管理 state(组件状态)和其它 React 特性的一组方法。React Hooks 改变了开发者编写 React 组件的方式,同时也加强了组件的可复用性和可维护性。本文详细地讨论了 React 3 个内置 Hook,分别为 useRefuseMemouseCallback。通过内置的 Hooks,我们可以很容易地在不同组件中复用相同的、带状态的逻辑,这也使得代码更具可维护性和易读性。

React Hooks 是一组可以在函数组件中管理组件状态、执行特定动作和访问上下方的钩子函数,包括 useState、useEffect、useRef、useMemo 和 useCallback 等。

useRef 提供了存储可变值的方法,该引用值会在组件 render 期间持续存在。useRef 通常用于访问或修改 DOM 元素的属性值,比如输入框的值(value)、容器的滚动位置(offsetLeft、offsetTop)。与 useState 不同的是,更新 useRef 的值并不会触发组件的重新渲染,相应地,可以直接通过 useRef 返回引用对象的属性值直接进行访问或修改。

useMemo 可以缓存函数的返回值,当且仅当依赖改变时,才会重新计算函数的返回值。useMemo 非常适合于计算密集的组件编写,比如组件的渲染依赖于某项复杂的数据结构计算结果。useMemo 的第 2 个参数是一个由依赖项组成的数组,只有其中依赖项改变时,函数才会重新计算。

useCallback 缓存并返回一个函数,当且仅当依赖项改变时,才会重新创建函数实例useMemo 适用于向子组件传递函数 prop ,从而减少子组件渲染的场景,其第 2 个参数也是一个依赖项数组,若依赖项改变时,函数才会重新创建。

一般情况下,React Hooks 可以将带状态逻辑(stateful logic)组件分解成更小,可组合的函数组件,这使得更容易地编写可复用和可维护的代码。使用 React Hooks 可以简化编写 React 组件过程,减少重复代码,使组件代码更具可读性

好了,现在让我们更深入地理解这些 Hooks 吧。

useRef

在 React 中,useRef 可以创建并存储一个可变的引用值,相当于在类组件中的 ref 属性,但也有些许不同。

useRef 使用一个初始值作为参数,并返回一个可变的引用对象。引用对象的属性值可以直接地被访问和修改,并且值的改变不会造成组件的重新渲染。这对于要延后访问或设置 DOM 元素属性的场景十分有用

下面是使用 useRef 来保存引用对象,实现点击按钮完成输入框定焦的一个示例:

import { useRef } from "react";

function ExampleUseRef() {
  const inputRef = useRef<HTMLInputElement>(null);
  function handleClick() {
    inputRef?.current?.focus();
  }
  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

上述代码中,ExampleUseRef 是一个使用 useRef 保存 RefObject(T 为 HTMLInputElement)的引用对象,引用的是一个 HTMLInputElement 对象。让我们进一步讨论下代码:

我们调用 useRef<HTMLInputElement>(null) 创建了一个 HTMLInputEelement 的引用,inputRef 的初始值为该引用。

用户点击“Focus Input”按钮时,handleClick 函数会被调用。在 handleClick 函数中,访问 inputRef 对象的 current 属性并调用 focus() 方法。

ExampleUseRef 组件返回一个包含 input 和 button 元素的 div 元素,input 元素的 ref 属性设置为 inputRef,这样就会创建一个 input 元素的引用。同时,把 button 元素的 onClick 属性设置为 handleClick 函数,当按钮被点击时,该函数会被调用。

综上所述,示例演示了如何在函数组件中创建一个 HTMLInputElement 的引用,然后通过 button 的 onClick 属性设置回调函数,最后在函数中使用引用对象的 current 属性调用 focus() 方法。通过使用 useRef,input 元素的引用会在组件渲染期间持续持有,这对于后期要访问或修改引用对象的属性非常有帮助。

useMemo

useMemo 缓存了计算密集型函数的执行结果,从而提高程序的性能。缓存技术是根据参数缓存函数的计算结果,如果传入相同的参数时,直接返回缓存结果,避免了重新计算结果。

useMemo 使用两个参数:一个是返回缓存值的函数,另一个是一组包含依赖项的数组。该函数只有在依赖项改变时,函数才会重新执行。如果依赖项在前后两次渲染相同时,则直接返回前一次缓存的函数返回值。下面是使用 useMemo 的详细示例:

import { useMemo } from "react";

function ExampleUseMemo({ a, b }: { a: number; b: number }) {
  const memoizedValue = useMemo(() => {
    return a * b;
  }, [a, b]);

  return <div>Memoized Value: {memoizedValue}</div>;
}

在上述示例中,useMemo 缓存了 a 和 b 的计算结果,这就意味着,如果 a 和 b 不发生改变,函数不会重新执行并且返回缓存值。这么做可以减少程序的计算时间,从而提高程序性能。

然而,值得注意的是,不是所有的场景都适合使用 useMemo。如果计算过程并不复杂或者组件不依赖于缓存值且频繁需要渲染,使用 useMemo 的性能提升并不会太显著,相反地,可能还会造成性能的损失。所以,我们要记住:如果缓存值是频繁修改的,相比于缓存计算值,直接渲染组件可能是更好的解决方案

总结下来,useMemo 是一个优化 React 程序的性能有用的工具,但是在使用时,需要综合考虑其优缺点。

useCallback

useCallback 通过减少组件的非必要渲染从而提升程序的性能,该 Hook 会缓存一个函数,只有在依赖项改变时,useCallback 才会返回一个新的函数实例

下面是 useCallback 的基础语法:

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

useCallback 的语法和 useEffect 相似,第 1 个参数是你要缓存的函数,第 2 个参数是一个包含依赖项的可选数组。如果依赖项发生改变,则会重新计算要缓存的函数并返回。使用 useCallback 的一个好处是,它可以帮助程序减少组件不必要的渲染次数

下面我们通过一个简单示例进行学习:

import { useEffect, useState } from "react";

function ParentComponentWithoutUseCallback() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <button onClick={handleClick}>Increment count</button>
      <ChildComponentWithoutUseCallback />
    </div>
  );
}

function ChildComponentWithoutUseCallback() {
  const [value, setValue] = useState(0);
  const expensiveFunction = () => {
    console.log("call expensiveFunction");
    // 计算密集
  };
  useEffect(() => {
    expensiveFunction();
  }, [expensiveFunction]);
  return <div>{value}</div>;
}

这是一个包含两个 React 组件(ParentComponentWithoutUseCallback 和 ChildComponentWithoutUseCallback)的示例:

ParentComponentWithoutUseCallback:组件使用 useState 定义了一个状态变量 count,其初始值为 0,同时定义了一个 handleClick 的点击回调函数,其内部实现使用 setCount 实现 count 状态值的加 1 操作。

ParentComponentWithoutUseCallback 组件渲染时,页面上会渲染出一个带 button 元素的 div 元素。当 button 发生点击时,handleClick 函数会增加 count 值,但同时 ChildComponentWithoutUseCallback 组件也会发生渲染。

ChildComponentWithoutUseCallback:组件使用 useState 定义了一个状态变量 value,其初始值为 0,同时还定义了一个模拟复杂计算的函数 expensiveFunction。

当 ChildComponentWithoutUseCallback 组件完成挂载时,useEffect 内部会执行 expensiveFunction 函数。useEffect 函数使用两个参数,分别为一个函数和一个包含依赖项的数组。在这个示例中,第 1 个参数是 expensiveFunction 函数,第 2 个参数是包含 expensiveFunction 函数的数组。将 expensiveFunction 函数作为 useEffect 的依赖项,只有当 expensiveFunction 引用改变时,useEffect 才会重新执行 expensiveFunction 函数。

最后,ChildComponentWithoutUseCallback 组件返回一个带 value 状态变量的 div 元素。当然,这个示例也演示了如何使用 useState 和 useEffect 来管理组件状态和执行 efffect(副作用)。当 ParentComponentWithoutCallback 组件更新了 count 状态变量时,ChildComponentWithoutCallback 组件也会根据 useEffect 再次执行计算密集的 expensiveFunction 函数。

为了优化上述代码,我们计划使用 useCallback 来缓存计算密集的函数,只有当依赖项改变时,计算密集函数才会重新计算:

import { useEffect, useState } from "react";

function ParentComponentUseCallback() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };
  const memoizedFunction = useCallback(() => {
    console.log("call memoizedFunction");
    // 计算密集
  }, []);
  return (
    <div>
      <button onClick={handleClick}>Increment count</button>
      <ChildComponentUseCallback expensiveFunction={memoizedFunction} />
    </div>
  );
}

function ChildComponentUseCallback({
  expensiveFunction,
}: {
  expensiveFunction: () => void;
}) {
  const [value, setValue] = useState(0);
  useEffect(() => {
    expensiveFunction();
  }, [expensiveFunction]);
  return <div>{value}</div>;
}

在上述示例中,除了使用 useCallback 定义 memoizedFunction 缓存函数,ParentComponentUseCallback 与之前的父组件类似。useCallback 用于缓存函数,所以不会每次渲染中重新创建。也正因如此,如果将缓存函数作为子组件的 prop,就可以减少子组件不必要的渲染次数

在 ChildComponentUseCallback 子组件中,除了定义了一个 expensiveFunction prop 外,其它的与之前的子组件类似。因为 expensiveFunction 函数作为 prop 传入子组件,所以需要在父组件中定义 expensiveFunction 函数。在这个示例中,只有当 expensiveFunction 改变时,useEffect 才会重新执行 expensiveFunction。然而,在父组件中,我们使用了 useCallback 缓存计算密集函数,当父组件发生渲染,memoizedFunction 不会发生改变,从而避免了子组件的再次渲染。

最后,子组件返回一个带 value 状态变量的 div 元素。

简单点说,示例演示了在函数组件中如何使用 useCallback 才避免组件的多次渲染。同时,在示例中也结合了 useEffect 技术,将计算密集函数作为组件的 prop,从而减少计算密集函数的执行

最终想法

总结来说,React Hooks 给开发者提供了在函数组件编写中管理组件状态更为简单的方式。

useRef 创建了一个 DOM 元素的可变的引用,在渲染中持续存在,允许开发者在造成组件重新渲染下直接访问或修改元素的属性。useMemo 可用于缓存计算密集型函数的返回值,只有当依赖项改变时才会重新计算值。useCallback 则是通过缓存一个函数,也是只当依赖项改变时,才会重新创建函数实例。

使用这些内置的 Hooks,如 useRef、useMemo 和 useCallback,开发者可以更高效管理组件状态,执行特定动作和访问上下文。通过将状态逻辑拆分到更小、可组合的函数组件中,使用 Hooks 编写出的组件更加简单,其可复用性、可读性得到加强。