React Hooks 详解:内置 Hooks、自定义 Hooks 与 Class 组件对比

A8平台 前端核心是 React 框架。由于是从 16 版本开始开发的,后来混合使用了 18 (18.17.1) 版本。所以存在 Class组件函数式组件 共存使用的情况。这里探讨 React 的 Hooks 系统,包括内置 Hooks 的用途、自定义 Hooks 的实现逻辑,以及与传统 Class 组件的对比。以便同事们能更好地从 Class组件 转到 函数式组件

本文内容结构如下:

  • 简要对比:Hooks 与 Class 组件的快速对比(表格形式)。
  • 详细说明:React 内置 Hooks 的全面描述。
  • 相关例子:实际代码示例。
  • 为什么如此设计及与 Class 传统对比的优点:设计理念和优势分析。

1. 简要对比

以下是 React 函数式组件(使用 Hooks)与 Class 组件的对比,采用表格形式呈现:

方面 Hooks(函数式组件) Class 组件 差异
状态管理 useState / useReducer(声明式,Fiber 节点存储) this.state / setState(实例绑定) Hooks 更简洁,无需 this,逻辑更集中。
副作用管理 useEffect / useLayoutEffect(自动清理) componentDidMount / DidUpdate / WillUnmount(手动管理) Hooks 统一副作用,自动清理,减少手动代码。
性能优化 useMemo / useCallback(细粒度记忆化) shouldComponentUpdate / PureComponent(组件级) Hooks 控制更灵活,性能优化更精准。
逻辑复用 自定义 Hooks(函数组合) HOC / Render Props(嵌套复杂) Hooks 复用更自然,代码更清晰,避免“包装地狱”。

2. 详细说明

React Hooks 是 React 16.8 引入的功能,允许函数式组件管理状态和副作用。内置 Hooks 分为基础和高级两类,均依赖 React 的 Fiber 架构(一个内部渲染引擎,用于存储组件状态和副作用)。

基础 Hooks

  • useState
  • 用途:声明和管理组件本地状态,返回 [state, setState] 数组。
  • 存储:状态存储在 Fiber 节点的 memoizedState 中,支持惰性初始化。
  • 场景:表单输入、计数器等简单状态管理。

  • useEffect

  • 用途:处理副作用(如异步请求、订阅、定时器),在渲染后执行,支持依赖数组和清理函数。
  • 存储:副作用存储在 Fiber 节点的 updateQueue 中,清理函数在卸载或依赖变化时执行。
  • 场景:数据获取、事件监听、DOM 操作。

  • useContext

  • 用途:访问 React Context 值,避免 props 逐层传递。
  • 存储:通过 Fiber 节点的上下文链访问 Context。
  • 场景:主题切换、全局配置(如语言、用户数据)。

高级 Hooks

  • useReducer
  • 用途:管理复杂状态逻辑,类似 Redux 的 reducer 模式。
  • 存储:状态存储在 Fiber 节点的 memoizedState 中。
  • 场景:复杂表单、状态机。

  • useCallback

  • 用途:返回记忆化的回调函数,仅在依赖变化时重新创建。
  • 存储:函数存储在 Fiber 节点的 memoizedState 中。
  • 场景:优化子组件渲染,防止函数引用变化。

  • useMemo

  • 用途:记忆化计算结果,仅在依赖变化时重新计算。
  • 存储:值存储在 Fiber 节点的 memoizedState 中。
  • 场景:昂贵计算(如数据过滤、排序)。

  • useRef

  • 用途:创建在组件生命周期内持久的引用对象。
  • 存储:引用存储在 Fiber 节点的 memoizedState 中,current 属性可变。
  • 场景:DOM 引用、持久化变量(如定时器 ID)。

  • useImperativeHandle

  • 用途:配合 forwardRef,自定义暴露给父组件的 ref 方法。
  • 存储:与 useRef 结合,操作 Fiber 节点的 ref。
  • 场景:封装组件内部方法,供父组件调用。

  • useLayoutEffect

  • 用途:同步执行副作用,适合 DOM 布局相关操作。
  • 存储:类似 useEffect,但在同步阶段执行。
  • 场景:测量 DOM 元素尺寸。

  • useDebugValue

  • 用途:在 React DevTools 中为自定义 Hook 显示调试信息。
  • 存储:仅用于调试,不影响 Fiber 状态。
  • 场景:调试自定义 Hook 状态。

实验性/新 Hooks(React 18+)

  • useTransition
  • 用途:标记非紧急状态更新为“过渡”,支持并发渲染。
  • 场景:优化复杂状态更新(如搜索过滤)。
  • 存储:利用 Fiber 的并发机制。

  • useDeferredValue

  • 用途:延迟低优先级值的更新,优先渲染高优先级内容。
  • 场景:优化大数据列表渲染。
  • 存储:与并发渲染相关,延迟值存储在 Fiber 节点。

  • useId

  • 用途:生成唯一 ID,适合服务端/客户端渲染一致性。
  • 场景:表单控件 ID、无障碍属性。
  • 存储:ID 由 React 内部生成,绑定到 Fiber。

  • useSyncExternalStore

  • 用途:订阅外部状态(如浏览器 API、第三方库),与 React 状态同步。
  • 场景:集成 Redux、MobX 或订阅 window.resize。
  • 存储:通过 Fiber 管理订阅和快照。

自定义 Hooks

自定义 Hooks 是基于内置 Hooks 封装的函数(因为外部除了使用系统hooks函数,没有办法操作 Fiber),用于复用状态和副作用逻辑。例如,useRequest(如 ahooks)使用 useState 管理请求状态,useEffect 处理异步操作,useRef 保存定时器实现防抖。


3. 相关例子

示例 1: 基础状态管理(useState vs Class)

Hooks 版本

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Class 版本

import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>Increment</button>
      </div>
    );
  }
}

示例 2: 副作用与防抖(useEffect vs Class)

Hooks 版本(防抖请求):

import { useState, useEffect } from 'react';

function DebounceFetch({ param }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const timer = setTimeout(() => {
      fetch(`/api?param=${param}`)
        .then(res => res.json())
        .then(setData);
    }, 300);
    return () => clearTimeout(timer); // 自动清理
  }, [param]);

  return <p>Data: {JSON.stringify(data)}</p>;
}

Class 版本

import React, { Component } from 'react';

class DebounceFetch extends Component {
  constructor(props) {
    super(props);
    this.state = { data: null };
    this.timeoutId = null;
  }

  componentDidUpdate(prevProps) {
    if (prevProps.param !== this.props.param) {
      clearTimeout(this.timeoutId);
      this.timeoutId = setTimeout(() => {
        fetch(`/api?param=${this.props.param}`)
          .then(res => res.json())
          .then(data => this.setState({ data }));
      }, 300);
    }
  }

  componentWillUnmount() {
    clearTimeout(this.timeoutId); // 手动清理
  }

  render() {
    return <p>Data: {JSON.stringify(this.state.data)}</p>;
  }
}

示例 3: 自定义 Hook(useRequest)

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

function useRequest(fetchFn, { manual = false, debounceInterval = 0 }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const timeoutRef = useRef(null);

  const run = useCallback(async (...args) => {
    setLoading(true);
    if (debounceInterval > 0) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = setTimeout(async () => {
        try {
          setData(await fetchFn(...args));
          setError(null);
        } catch (err) {
          setError(err);
        } finally {
          setLoading(false);
        }
      }, debounceInterval);
    } else {
      try {
        setData(await fetchFn(...args));
        setError(null);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }
  }, [fetchFn, debounceInterval]);

  useEffect(() => {
    return () => clearTimeout(timeoutRef.current); // 清理定时器
  }, []);

  useEffect(() => {
    if (!manual) run();
  }, [manual, run]);

  return { data, loading, error, run };
}

// 使用示例
function App() {
  const fetchData = async (param) => {
    const response = await fetch(`/api?param=${param}`);
    return response.json();
  };

  const { data, loading, run } = useRequest(fetchData, {
    manual: true,
    debounceInterval: 300,
  });

  return (
    <div>
      <button onClick={() => run('test')}>Fetch Data</button>
      {loading ? <p>Loading...</p> : <p>Data: {JSON.stringify(data)}</p>}
    </div>
  );
}

4. 为什么如此设计及与 Class 传统对比的优点

设计理念

React Hooks 的设计目标是推动函数式编程范式,解决 Class 组件的痛点,同时适配 React 的 Fiber 架构: - 弥补函数式组件局限:传统函数式组件无状态、无生命周期。Hooks 通过 Fiber 节点的 memoizedState 链表存储状态,按调用顺序绑定逻辑,无需 this。 - 逻辑集中:Class 组件的生命周期方法(如 componentDidMountcomponentDidUpdate)分散逻辑,Hooks 用 useEffect 按功能分组。 - 逻辑复用:Class 组件依赖 HOC 或 Render Props,易导致“包装地狱”。自定义 Hooks 是函数组合,更自然、易读。 - 并发支持:Hooks(如 useTransitionuseDeferredValue)适配 React 的并发渲染(如 Concurrent Mode),Class 组件不支持。 - 调试与一致性:Hooks 支持 DevTools(useDebugValue)和 SSR/CSR 一致性(useId)。

与 Class 组件对比的优点

  • 简洁性:Hooks 无需 this 绑定(避免 bind 或箭头函数),代码量减少 20-50%。例如,useStatethis.setState 更直观。
  • 易读/维护:逻辑按功能分组(useEffect 块),而 Class 组件按生命周期分散。自定义 Hooks 复用逻辑更清晰。
  • 自动清理useEffect 自动清理副作用(如定时器、订阅),Class 组件需手动在 componentWillUnmount 中清理,易漏。
  • 细粒度优化useMemouseCallback 针对特定值/函数记忆化,Class 组件的 PureComponent 是组件级,控制较粗糙。
  • 灵活性:Hooks 可在条件/循环中使用(需遵守规则),Class 组件生命周期固定。Hooks 支持并发渲染,未来-proof。
  • 缺点:Hooks 规则严格(顶层调用、无条件),学习曲线稍陡。Class 组件更适合传统 OOP 开发者,但 Hooks 是 React 官方推荐方向。

迁移建议

  • 从 Class 到 Hooks:将 this.state 替换为 useState/useReducer,生命周期方法转为 useEffect,优化用 useMemo/useCallback
  • 自定义 Hooks:封装复用逻辑(如 useRequest),减少重复代码。
  • 参考库:研究 ahooksuseRequest 源码,学习防抖、节流、缓存实现。

5. Class 和函数式组件混合使用的注意点

在 React 项目中,Class 组件和函数式组件(Hooks)可以混合使用,这是 React 设计支持的特性,尤其在渐进式迁移时非常有用。React 的 Fiber 架构(渲染引擎)统一处理两者,确保兼容性。但混合使用时需注意一些潜在问题,如上下文传递、ref 处理、props 管理等。下面详细说明关键注意点,包括函数式组件包裹 Class 组件的处理,以及使用 useContext 优化全局状态管理(代替 props 层层穿透)。

5.1 总体注意点

兼容性:React 支持混合渲染树(例如,函数式组件作为父组件包裹 Class 子组件,或反之)。Fiber 架构会将两者转换为统一的 Fiber 节点进行渲染和协调(reconciliation),无需特殊配置。 性能影响:混合使用不会显著影响性能,但如果 Class 组件使用旧生命周期(如 componentWillReceiveProps),可能在并发模式下有兼容问题。建议避免使用已弃用的生命周期方法。 迁移策略:逐步替换 Class 组件为函数式组件。混合阶段,确保 props 和状态一致传递。 调试:在 React DevTools 中,Class 组件显示为类名,函数式组件显示为函数名。混合时,检查组件树以避免上下文丢失。 规则遵守:函数式组件必须遵守 Hooks 规则(顶层调用、无条件),Class 组件不受影响。

5.2 函数式组件包裹原来的 Class 组件:如何处理

当函数式组件作为父组件包裹 Class 子组件时,处理相对简单,但需注意 props 传递、ref 和上下文。

props 传递:

正常通过 JSX 传递 props。Class 组件的 this.props 会接收到函数式组件传入的值。 注意:如果函数式组件使用 useMemo 或 useCallback 优化 props,确保 props 引用稳定,避免 Class 组件不必要的 componentDidUpdate 触发。

ref 处理:

如果需要访问 Class 子组件的实例,使用 useRef 在函数式父组件中创建 ref,并通过 ref 属性传递。 Class 组件支持直接 ref(指向实例),但函数式组件默认不支持(需用 forwardRef)。 注意点:避免在 ref 中直接操作 Class 组件的内部状态,这可能违反 React 的单向数据流。

上下文(Context)传递:

如果函数式父组件使用 useContext,Class 子组件可以通过 static contextType 或 访问同一 Context。 确保 Context Provider 在树的上层包裹两者。

示例:函数式组件包裹 Class 组件函数式父组件:

import { useState, useRef } from 'react';
import MyClassChild from './MyClassChild'; // 假设这是 Class 组件

function FunctionalParent() {
  const [value, setValue] = useState('Hello');
  const childRef = useRef(null);

  const handleClick = () => {
    if (childRef.current) {
      childRef.current.someMethod(); // 调用 Class 组件的方法
    }
  };

  return (
    <div>
      <MyClassChild ref={childRef} message={value} />
      <button onClick={handleClick}>Call Child Method</button>
    </div>
  );
}

Class 子组件:

import React, { Component } from 'react';

class MyClassChild extends Component {
  someMethod() {
    console.log('Called from parent');
  }

  render() {
    return <p>Message from parent: {this.props.message}</p>;
  }
}

export default MyClassChild;

潜在问题及解决:

  • props 变化检测:Class 组件的 componentDidUpdate 可检测 props 变化,但如果函数式父组件频繁渲染,可能导致性能问题。解决:使用 React.memo 包裹 Class 组件(需转换为函数式包装器)
  • 错误边界:Class 组件支持 componentDidCatch 作为错误边界,函数式组件需用自定义 Hook 或库实现
  • Fiber 架构兼容:Fiber 处理混合树时,确保没有循环依赖或无限渲染。测试并发模式下行为(如果启用)

5.3 使用 useContext 管理全局对象(优化 props 层层穿透)

在 Class 组件中,全局状态往往通过 props drilling(层层传递)管理,这会导致代码冗长和维护困难。

混合使用时,可以引入 React Context API,并用 useContext 在函数式组件中消费,而 Class 组件兼容旧方式或直接使用 Context。

为什么优化:

  • props drilling 增加组件耦合,难以追踪。Context 提供“全局”访问,减少中间层 props。
  • Fiber 架构支持 Context 的高效更新,通过 Fiber 节点的上下文链传播变化。

实现步骤:

  1. 创建 Context Provider(通常在根组件)。
  2. 函数式组件使用 useContext 消费。
  3. Class 组件使用 static contextType 或 消费。
  4. 结合 useState 或 useReducer 管理 Context 值,实现全局状态。
示例:使用 Context 管理全局主题Context 创建:
import { createContext, useState } from 'react';

export const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
函数式组件消费:
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function FunctionalComponent() {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <div style={{ background: theme === 'light' ? '#fff' : '#000' }}>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </div>
  );
}
Class 组件消费:
import React, { Component } from 'react';
import { ThemeContext } from './ThemeContext';

class ClassComponent extends Component {
  static contextType = ThemeContext; // 或使用 <Consumer>

  render() {
    const { theme } = this.context;
    return <p>Current Theme: {theme}</p>;
  }
}
根组件使用:
function App() {
  return (
    <ThemeProvider>
      <FunctionalComponent />
      <ClassComponent />
    </ThemeProvider>
  );
}

注意点:

  • 性能:Context 更新会触发所有消费者重新渲染。解决:使用多个 Context 分离关注点,或结合 useMemo 优化。
  • 混合兼容:确保 Class 组件正确设置 contextType。如果 Class 组件嵌套深层,使用
  • 与 Redux/MobX:如果项目已有全局状态库,可用 useSyncExternalStore 桥接。 Fiber 架构益处:Fiber 的上下文传播高效,支持并发更新(例如,useTransition 延迟 Context 变化)。
  • 潜在问题:Context 不适合高频更新(会导致大范围重渲染)。对于混合项目,逐步迁移 props drilling 到 Context。

5.4 其他混合注意点

  • 事件处理:函数式组件的事件函数可用 useCallback 稳定,传递给 Class 子组件避免不必要渲染。
  • 测试:混合时,使用 React Testing Library 测试组件树,确保上下文和 ref 正常。
  • 版本兼容:React 18+ 支持混合,但旧项目需升级以利用并发特性。
  • 推荐:在混合阶段,优先将叶节点 Class 组件迁移到函数式,逐步向上。最终目标:全函数式组件 + Hooks。

总结:React Hooks 通过 Fiber 架构为函数式组件带来状态和副作用管理,简化开发、提升复用性,是现代 React 开发的首选。Class 组件虽适合传统场景,但 Hooks 的优势使其成为未来趋势。A8平台 前端新开发的功能组件,基本上都采用函数式组件方式,标准组件库依赖也更新到 18+ 版本。混合使用时,关注兼容性和优化全局状态,能平滑过渡。

2025-09-15 于北京用友产业园