从类组件到函数组件:深入理解 React Hooks

157 阅读9分钟

React 的发展历程中,类组件一度是状态管理和生命周期方法的主要形式。然而,类组件的使用也带来了复杂性,如 this 关键字的混淆使用、重复的生命周期代码等。React Hooks 是自 React 16.8 版本引入的一种功能,允许函数组件执行更复杂的操作,如状态管理、副作用处理、上下文订阅等。这篇文章将全面探讨 Hooks 的基础和进阶用法,帮助开发者深入理解并有效利用这些功能。

为什么需要 Hooks?

1. 简化组件逻辑

类组件经常需要编写重复的生命周期方法如 componentDidMountcomponentDidUpdatecomponentWillUnmount 来添加副作用处理(如数据请求)。Hooks 允许你在不分离组件生命周期的情况下直接在函数组件中使用副作用,简化代码并提高可维护性。

2. 函数组件增强

在 Hooks 出现之前,函数组件被视为无状态的,只能用于呈现 UI,但不能持有状态或使用生命周期钩子。Hooks 的引入改变了这一局限,为函数组件提供了使用内部状态、副作用处理、上下文订阅等能力。

3. 更好的代码复用

创建自定义 Hooks 可以让你抽取组件逻辑到可重用的函数中。与高阶组件和渲染道具模式相比,自定义 Hooks 提供了一种更清晰和更容易共享逻辑的方式。

常用 React Hooks 介绍

  • useState
  • useEffect
  • useMemo
  • useCallback
  • useContext
  • useReducer
  • useRef

1. useState

useState 是最基础的 Hook,用于在函数组件中添加 state。

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
};

export default Counter;

  • useState 返回一个数组,包含当前 state 的值和更新 state 的函数。
  • 通过 useState(0) 初始化 count 为 0。

2. useEffect

useEffect 是处理函数组件副作用的主要方式。副作用包括数据订阅、定时器、日志、手动更改 DOM 等副作用逻辑。

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

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      const userData = await response.json();
      setUser(userData);
    };

    fetchData();
  }, [userId]);  // 依赖于 userId,userId 改变时重新执行

  return (
    <div>
      {user ? <p>{user.name}</p> : <p>Loading...</p>}
    </div>
  );
};
  • useEffect 可以看作是 componentDidMountcomponentDidUpdatecomponentWillUnmount 的组合。
  • useEffect 的第一个参数是一个函数,包含副作用逻辑。第二个参数是依赖项数组,只有当依赖项变化时,useEffect 才会重新执行。
  • 如果为空数组,表示只在组件挂载和卸载时执行
  • 依赖数组 [userId] 指定了 effect 仅在 userId 变化时运行。

3. useMemo

useMemo 用于在函数组件中缓存计算结果。它接受两个参数:

  1. 一个函数,用于计算需要缓存的值。
  2. 一个依赖项数组,当数组中的某个值发生变化时,才会重新计算缓存的值。

useMemo 返回的是缓存的计算结果,可以避免在每次渲染时都重复执行耗时的计算

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

// 不使用useMemo
const Fibonacci = ({ num }) => { 
    console.log('Calculating Fibonacci...'); 
    const fibCalc = (n) => (n <= 1 ? n : fibCalc(n - 1) + fibCalc(n - 2)); 
    const fib = fibCalc(num); 
    return <div>Fibonacci of {num} is {fib}</div>; };

// 使用useMemo
const Fibonacci = ({ num }) => {
  console.log('Calculating Fibonacci...');
  // fibCalc是一个递归函数,用于计算 Fibonacci 数列。
  const fib = useMemo(() => {
    const fibCalc = (n) => (n <= 1 ? n : fibCalc(n - 1) + fibCalc(n - 2));
    return fibCalc(num);
  }, [num]);

  return <div>Fibonacci of {num} is {fib}</div>;
};

const App = () => {
  const [num, setNum] = useState(10);
  const [inputNum, setInputNum] = useState(10);
  const handleCalculate = () => { setNum(inputNum); };
  return (
    <div>
      <input type="number" value={inputNum} onChange={(e) => setInputNum(parseInt(e.target.value))} />
      <button onClick={handleCalculate}>Calculate</button>
      <Fibonacci num={num} />
    </div>
  );
};

export default App;

useMemo 用于缓存 Fibonacci 数列的计算结果。Fibonacci 数列的计算可能会非常耗时,尤其是在递归深度较大时,因此使用 useMemo 可以显著提升性能。

  1. 每次输入框改变时

    • inputNum 更新,但 Fibonacci 计算不触发。
    • Fibonacci 组件不会重新渲染,因为 num 没有变化。
  2. 点击“Calculate”按钮时

    • setNum(inputNum) 更新 num
    • useMemo 检测到 num 变化,触发 Fibonacci 重新计算。
  • 不使用 useMemo:每次 inputNum 更新时都会触发 Fibonacci 计算,无论 num 是否改变。
  • 使用 useMemo:只有当 num 变化时才会触发 Fibonacci 计算,避免了不必要的计算,提高了性能。

4. useCallback

useCallback 用于返回一个记忆化的回调函数。它接受两个参数:

  1. 一个回调函数。
  2. 一个依赖项数组。

只有在依赖项发生变化时,useCallback 才会重新创建该回调函数。这对于优化性能非常有用,特别是在回调函数被传递给子组件时,避免子组件因函数重建而不必要的重新渲染。

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

// React.memo 是一个高阶组件,用于优化函数组件的性能。
// 它通过浅层对比 props,只有在 props 发生变化时才重新渲染组件。
// 这类似于类组件中的 `shouldComponentUpdate` 方法。
const Button = React.memo(({ onClick, children }) => {
  console.log('Button re-render');
  return <button onClick={onClick}>{children}</button>;
});

const App = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Button onClick={increment}>Increment</Button>
    </div>
  );
};

export default App;
  • useCallback 接受一个回调函数 () => setCount(c => c + 1) 和一个依赖项数组 []
  • 由于依赖项数组为空,increment 函数在组件生命周期内只会被创建一次。
  • increment 函数被传递给子组件 Button 作为 onClick 属性。
  • 由于 increment 函数不会在每次渲染时都重新创建,因此 Button 组件不会因 onClick 属性的变化而重新渲染。

React.memo 包装了 Button 组件, 只有在 Button 组件的 props 发生变化时,Button 组件才会重新渲染。

onClickchildren 作为 props 传递给 Button。由于 useCallback 确保 increment 函数不变,Button 组件不会因 onClick 变化而重新渲染。

5. useContext

useContext用于订阅 React 上下文(Context)。它使得我们可以在函数组件中轻松地访问上下文数据,而无需通过高阶组件或渲染道具(render props)来传递。

import React, { useContext, createContext, useState } from 'react';


//使用createContext创建一个上下文对象。
const ThemeContext = createContext();

const ThemedButton = () => {
//在子组件中,使用useContext来访问上下文值。
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme.background, color: theme.color }}>Themed Button</button>;
};

const App = () => {
  const [theme, setTheme] = useState({ background: 'black', color: 'white' });

//在应用组件树中使用ThemeContext.Provider提供上下文值
  return (
    <ThemeContext.Provider value={theme}>
      <ThemedButton />
      <button onClick={() => setTheme({ background: 'white', color: 'black' })}>Change Theme</button>
    </ThemeContext.Provider>
  );
};

export default App;
  1. 创建上下文
  • createContext 创建了一个上下文对象 ThemeContext
  • 这个对象包含一个 Provider 组件,用于提供上下文值,以及一个 Consumer 组件,用于订阅上下文。
  1. 提供上下文
  • ThemeContext.Provider 提供 theme 对象作为上下文值。
  • ThemedButton 组件和其他子组件可以访问这个上下文值。
  1. 消费上下文
  • useContext Hook 使 ThemedButton 组件能够访问 ThemeContext 提供的 theme 值。
  • 通过 useContext(ThemeContext),我们获得当前的上下文值 theme,并使用它来设置按钮的样式。
  1. 简化数据传递:通过上下文,可以在组件树中任何位置访问数据,而无需通过每层组件传递 props。
  2. 代码更清晰:使用 useContext 和上下文提供者,可以使组件的依赖关系更加明确,代码更易于理解和维护。
  3. 减少样板代码:相比于传统的高阶组件和渲染道具模式,使用上下文可以减少样板代码。
  4. 避免滥用:上下文主要用于全局或跨层级的数据共享,过度使用可能导致组件重渲染和性能问题。
  5. 性能优化:在提供上下文时,尽量将上下文值拆分为多个上下文,以减少不必要的重渲染。

6. useReducer

useReduceruseState 的替代方案,适用于管理复杂的 state 逻辑。它接收一个 reducer 函数和一个初始 state,并返回当前的 state 以及与之对应的 dispatch 方法。

什么时候使用useReducer

  • 当 state 逻辑复杂且包含多个子值时。
  • 当下一个 state 依赖于之前的 state 时。
  • 当你希望通过集中管理 state 更新逻辑,使得代码更具可预测性和可维护性时。
import React, { useReducer } from 'react';

// initialState 定义了计数器的初始状态,初始值为 0
const initialState = { count: 0 };

// reducer函数接收当前的 state 和 action,并返回新的state
// 根据action.type的不同,reducer函数会更新count
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

const Counter = () => {
// useReducer接收reducer函数和initialState,返回当前的state和 dispatch方法
// state是当前的状态对象,dispatch是一个分发 action 的函数
  const [state, dispatch] = useReducer(reducer, initialState);

// dispatch函数用于分发 action对象,根据action的类型来更新状态
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
};

export default Counter;

通过定义初始 state 和 reducer 函数,可以集中管理状态更新逻辑,使代码更具可预测性和可维护性。在需要管理复杂状态逻辑的场景中,useReducer 是一个非常有用的工具。

7. useRef

有时我们需要直接访问和操作 DOM 元素,或者保存跨渲染周期的可变值 ,useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。

  1. 访问 DOM 元素:通过 useRef 可以获取和操作 DOM 元素的引用。
  2. 保存可变值:可以用来保存跨渲染周期的可变值,而不会触发重新渲染。
import React, { useRef, useEffect } from 'react';

const FocusInput = () => {
// useRef返回一个 ref 对象,其初始值为undefined,这个对象在组件的整个生命周期内保持不变
  const inputRef = useRef();
// useEffect确保在组件挂载后调用inputRef.current.focus()方法,使得输入框自动获得焦点
// 依赖项数组[]确保这个副作用只在组件挂载和卸载时执行一次
  useEffect(() => {
    inputRef.current.focus();
  }, []);
// 通过将inputRef绑定到<input>元素的ref属性上,我们可以获取到这个 DOM 元素的引用
  return <input ref={inputRef} type="text" />;
};

export default FocusInput;

useRef 也可以用于保存跨渲染周期的可变值,例如存储前一次渲染的值

import React, { useState, useRef, useEffect } from 'react';

const PreviousValue = () => {
// count是当前计数值, prevCountRef 用于保存前一次渲染的计数值
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

// 每次count发生变化时,useEffect将当前的 count值保存到prevCountRef.current中
  useEffect(() => {
    prevCountRef.current = count;
  }, [count]);
   
// prevCount变量保存了前一次渲染的count值。
  const prevCount = prevCountRef.current;

  return (
    <div>
      <p>Current Count: {count}</p>
      <p>Previous Count: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default PreviousValue;

总结

React Hooks 为函数组件带来了强大的功能和灵活性。通过使用 useState 管理 state,useEffect 处理副作用,useMemouseCallback 优化性能,以及 useContextuseReduceruseRef 处理复杂的 state 逻辑和引用,你可以创建高效、简洁且易于维护的 React 应用。