React 之 Hooks

React 之 Hooks:让函数组件拥有 "超能力" 的魔法

在 React 的世界里,Hooks 就像一把打开函数组件潜能的钥匙。从 Class 组件到函数组件的演进中,Hooks 的出现彻底改变了 React 的开发模式。今天我们就深入探索 Hooks 的奥秘,从基础概念到实战技巧,带你全面掌握这一 React 核心特性。

一、初识 Hooks 三问

1.什么是 Hooks?

Hooks 是 "以use开头的函数",是 React 提供的一套 API,让函数组件能够拥有原本只有 Class 组件才有的状态(State)和生命周期特性。简单来说,Hooks 就像给函数组件装上了 "发动机",让它从 "静态展示" 升级为 "动态交互"。

比如我们熟悉的useStateuseEffect,都是 React 内置的 Hooks。它们的出现让 React 代码更贴近原生 JavaScript,函数式风格的写法也让逻辑更清晰。

2.早期没有 Hooks 有什么问题?

在 Hooks 出现之前,React 开发者主要依赖 Class 组件处理状态和副作用,但这带来了两个明显痛点:

  1. 生命周期函数混乱 :一个 Class 组件的componentDidMountcomponentDidUpdate里可能混杂着数据请求、事件监听、定时器等多种逻辑,维护时需要在多个生命周期间跳来跳去。
  2. 函数组件能力有限:纯函数组件只能接收 props(属性) 并返回 JSX,无法拥有自己的状态和副作用处理能力,限制了其应用场景。

3.Hooks 产生的原因?

Hooks 的诞生就是为了解决上述问题:

  • 让函数组件具备状态管理和副作用处理能力,统一组件写法(推荐函数组件 + Hooks)。
  • 使组件逻辑复用更简单,通过自定义 Hooks 可以轻松提取和共享逻辑。
  • 让相关逻辑聚合在一起(比如数据请求和清理操作),而非分散在不同生命周期中。

用一句话总结:Hooks 让 React 组件的逻辑更清晰、复用更简单、代码更易维护。

二、纯函数与副作用

理解纯函数与副作用,是掌握 Hooks 的基础 ------ 尤其是useEffect的设计核心就围绕这两个概念展开。

1.什么是纯函数?

纯函数是满足两个条件的函数:

  1. 相同输入一定产生相同输出:只要传入的参数不变,返回结果就绝对一致。
  2. 无副作用:不会修改函数外部的变量、不依赖外部状态、不产生网络请求 / 定时器等 "额外操作"。

代码示例

javascript

复制代码
// 纯函数示例
const add = function(x, y) {
  // 仅依赖输入参数x和y,无外部依赖
  // 运算过程中不会修改外部变量,也没有网络请求、定时器等操作
  return x + y; 
}

// 调用结果可预测:add(2,3)永远返回5,add(1,1)永远返回2
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 还是5

这个add函数就是典型的纯函数:它的输出完全由输入决定,且不会对外部环境造成任何影响。

2.什么是副作用?

副作用(Side Effect)是指函数在执行过程中,除了返回结果外,还对外部环境产生了额外影响,或者依赖外部环境的状态。

常见的副作用包括

  • 修改函数外部的变量(如数组、对象)
  • 发送网络请求(fetch/axios)
  • 操作 DOM(添加 / 删除元素)
  • 设置定时器 / 计时器(setInterval/setTimeout)
  • 注册 / 移除事件监听

代码示例

javascript

复制代码
// 副作用示例
function add(nums) {
  nums.push(3); // 修改了外部传入的数组(副作用:改变外部状态)
  return nums.reduce((pre, cur) => pre + cur, 0);
}

const nums = [1, 2];
add(nums); 
console.log(nums); // [1,2,3](外部数组被修改,结果不可预测)

这个add函数就有副作用:它修改了传入的nums数组(外部变量),导致调用后nums本身发生了变化。即使输入相同的nums,多次调用的结果也会不同(第一次返回 6,第二次返回 9),因为数组被持续修改。

3.为什么 React 强调纯函数?

React 组件的本质是 "输入 props,输出 JSX" 的函数。React 强调纯函数,是因为纯函数带来了可预测性可维护性

  • 可预测性:相同的 props 渲染出相同的 UI,方便调试和测试。
  • 可维护性:纯函数组件逻辑清晰,不依赖外部状态,更易理解和修改。

但实际开发中,组件不可能完全是纯函数(比如需要请求数据、设置定时器),因此 React 用useEffect来专门处理副作用,让组件主体保持纯函数特性,副作用逻辑单独管理。

三、核心常用 Hooks(日常开发高频使用)

React 提供了多个内置 Hooks,其中useStateuseEffect是日常开发中使用频率最高的两个,堪称 "Hooks 双子星"。

1. useState - 管理组件内部状态

(1)什么是状态?

状态(State)是组件内部管理的、可以变化的数据。当状态变化时,组件会重新渲染,UI 也会随之更新。比如计数器的数值、表单的输入内容、弹窗的显示 / 隐藏状态等,都是常见的状态。

(2)用途

useState的作用是在函数组件中创建和管理状态,让组件拥有 "记忆能力"------ 即使组件重新渲染,状态也能被保留。

(3)核心规则
  • 只能在函数组件或自定义 Hooks 的顶层调用(不能在 if、for 等条件 / 循环语句中使用)。
  • 每次组件渲染时,useState都会返回当前的状态和更新函数。
(4)代码示例

jsx

复制代码
// useState示例
import { useState } from 'react';

export default function App() {
  // 方式1:直接传入初始值(简单场景)
  // const [num, setNum] = useState(1);

  // 方式2:函数式初始化(复杂场景,初始值需要计算时使用)
  const [num, setNum] = useState(() => {
    // 函数体只会在组件首次渲染时执行(性能优化)
    const num1 = 1 + 2;
    const num2 = 2 + 3;
    return num1 + num2; // 初始值为6
  });

  return (
    // 点击时更新状态:使用函数式更新(依赖上一次状态时推荐)
    <div onClick={() => setNum(prevNum => { 
      console.log(prevNum); // 打印上一次的num值
      return prevNum + 1; 
    })}>
      {num} {/* 渲染当前状态 */}
    </div>
  )
}

效果亮个相:

代码解析

  • const [num, setNum] = useState(initialValue):通过数组解构获取状态(num)和更新函数(setNum)。
  • 函数式初始化:当初始值需要复杂计算时,传入一个函数,该函数的返回值作为初始值,且只会在组件首次渲染时执行,避免重复计算。
  • 函数式更新:setNum(prevNum => prevNum + 1)确保使用的是 "上一次的状态",在连续更新状态时更可靠(比如多次点击计数器)。

2. useEffect - 处理副作用

(1)用途

useEffect专门用于处理组件中的副作用,比如:

  • 发送网络请求获取数据
  • 设置 / 清除定时器、计时器
  • 注册 / 移除事件监听
  • 操作 DOM

它相当于 Class 组件中componentDidMount(挂载)、componentDidUpdate(更新)、componentWillUnmount(卸载)三个生命周期函数的组合。

(2)规则
  • useState,只能在函数组件或自定义 Hooks 的顶层调用。
  • 可以返回一个清理函数,用于清除副作用(如清除定时器、移除事件监听)。
(3)代码示例

jsx

复制代码
// useEffect示例
import { useEffect } from 'react';

export default function Demo() {
  useEffect(() => {
    // 组件挂载时执行(类似componentDidMount)
    console.log('组件挂载了');
    
    // 副作用:设置定时器
    const timer = setInterval(() => {
      console.log('定时器执行中...');
    }, 1000);

    // 清理函数:组件卸载时执行(类似componentWillUnmount)
    return () => { 
      console.log('组件卸载了');
      clearInterval(timer); // 清除定时器,避免内存泄漏
    }
  }, []); // 依赖数组为空

  return <div>副作用示例</div>;
}

效果亮个相:

代码解析

  • useEffect的第一个参数是一个回调函数,包含要执行的副作用逻辑。
  • 回调函数可以返回一个清理函数,在组件卸载或副作用重新执行前调用,用于 "收拾残局"(如清除定时器,避免内存泄漏)。
  • 第二个参数是依赖数组(下文详细讲解),这里为空数组表示 "只在组件挂载时执行一次,卸载时执行清理函数"。
(4)没有清除定时器,内存泄漏的情况举例
复制代码
import { useState, useEffect } from 'react';

// 被控制挂载/卸载的Demo组件(注释清理定时器代码,模拟泄漏)
function Demo() {
  useEffect(() => {
    // 组件挂载时执行
    console.log('Demo组件挂载了');
    
    // 副作用:设置每秒打印的定时器
    const timer = setInterval(() => {
      console.log('定时器执行中...(内存泄漏中)', new Date().toLocaleTimeString());
    }, 1000);

    // 【关键】注释清理函数,模拟"不清除定时器"的场景
    // return () => { 
    //   console.log('Demo组件卸载了');
    //   clearInterval(timer); // 注释掉这行,不清理定时器
    // }
  }, []); // 依赖数组为空

  return <div>副作用示例(定时器未清理)</div>;
}

// 父组件:通过开关控制Demo组件的挂载/卸载
export default function App() {
  // 控制Demo组件是否显示的开关
  const [showDemo, setShowDemo] = useState(true);

  return (
    <div style={{ padding: '20px' }}>
      {/* 开关按钮:控制Demo组件挂载/卸载 */}
      <button 
        onClick={() => setShowDemo(!showDemo)}
        style={{ padding: '8px 16px', marginBottom: '20px' }}
      >
        {showDemo ? '卸载Demo组件' : '挂载Demo组件'}
      </button>

      {/* 条件渲染:showDemo为true则挂载,false则卸载 */}
      {showDemo && <Demo />}
    </div>
  );
}

效果亮个相:

  • 初始状态:Demo 组件挂载,控制台会打印:

    复制代码
    Demo组件挂载了
    定时器执行中...(内存泄漏中) 10:00:00
    定时器执行中...(内存泄漏中) 10:00:01
    定时器执行中...(内存泄漏中) 10:00:02

    (每秒打印一次,符合预期)

  • 点击 "卸载 Demo 组件" 按钮

    • 现象 1:页面上的 Demo 组件消失(已卸载);
    • 现象 2:控制台仍然每秒打印定时器日志 !即使组件已经卸载,定时器还在后台运行,这就是内存泄漏------ 组件已销毁,但它创建的定时器还占用浏览器内存和 CPU 资源,直到页面刷新。
  • 多次点击 "挂载 / 卸载" 按钮

    • 现象:每挂载一次 Demo 组件,就会新增一个定时器;卸载后所有定时器都不会停止,控制台打印日志的频率会越来越快(比如挂载 2 次后,每秒打印 2 条日志),内存占用持续升高。

只要在 useEffect 中创建了定时器、事件监听等 "外部资源",必须在清理函数中销毁,不然可能出现资源叠加、内存泄漏,这是 React 开发的核心避坑点。

四、useEffect 接收的参数 --- 依赖数组

useEffect的灵活性很大程度上来自它的第二个参数 ------ 依赖数组。理解依赖数组是掌握useEffect的关键。

1.useEffect 接收哪些参数?

useEffect接收两个参数:

javascript

复制代码
useEffect(effectCallback, dependencies);
  • 第一个参数effectCallback:副作用逻辑的函数。
  • 第二个参数dependencies:依赖数组,一个包含变量的数组(可选)。

2.什么是依赖数组?作用是什么?

依赖数组是useEffect的 "触发器",它决定了effectCallback何时执行。React 会监听数组中变量的变化,当变量发生变化时,会先执行上一次的清理函数,再执行新的effectCallback

简单来说:依赖数组中的变量,就是副作用逻辑 "依赖" 的外部状态。当这些状态变化时,副作用需要重新执行。

3.依赖项的三种情况

(1) 依赖项为空数组 []
  • 执行时机 :仅在组件挂载时执行一次effectCallback,组件卸载时执行清理函数。

  • 适用场景:只需要初始化一次的操作,如获取初始化数据、设置全局事件监听(如滚动监听)。

  • 代码示例

    jsx

    复制代码
    // Demo.jsx中依赖为空数组的场景
    useEffect(() => {
      console.log('组件挂载时执行(只一次)');
      const timer = setInterval(() => {
        console.log('定时器运行中');
      }, 1000);
    
      return () => {
        console.log('组件卸载时执行');
        clearInterval(timer); // 清理定时器
      }
    }, []); // 空数组:不依赖任何状态
  • 避坑要点

    • 不要在回调函数中使用组件内的状态或 props,因为闭包会导致获取到的是初始值(不会随状态更新而变化)。
    • 必须在清理函数中清除所有可能导致内存泄漏的操作(如定时器、事件监听)。
(2) 依赖项为有值数组 [a, b, c]
  • 执行时机 :组件挂载时执行一次,之后每当abc中任意一个值发生变化时,先执行上一次的清理函数,再执行新的effectCallback

  • 适用场景:副作用逻辑依赖特定状态,当状态变化时需要重新执行,如根据 ID 获取数据、根据输入值过滤列表。

  • 代码示例

    jsx

    复制代码
    // App.jsx中依赖num的场景
    useEffect(() => {
      console.log('num变化了,重新执行副作用');
      // 依赖num的定时器:每次num变化时,定时器打印最新的num
      const timer = setInterval(() => {
        console.log('当前num:', num);
      }, 1000);
    
      return () => {
        console.log('清理上一次的定时器');
        clearInterval(timer); // 避免多个定时器同时运行
      }
    }, [num]); // 依赖num:num变化时触发
  • 避坑要点

    • 必须将所有在effectCallback中使用的状态、props、函数都加入依赖数组,否则可能获取到旧值(闭包问题)。
    • 避免依赖引用类型(如对象、数组),因为 React 采用 "浅比较",可能导致不必要的重复执行(可通过useMemo优化)。
(3) 无依赖项(省略第二个参数)
  • 执行时机 :组件挂载时执行一次,之后每次组件重新渲染时 都会先执行上一次的清理函数,再执行新的effectCallback

  • 适用场景:极少使用,一般用于需要 "每次渲染都执行" 的操作(如同步 DOM 到状态)。

  • 代码示例

    jsx

    复制代码
    // App.jsx中无依赖项的场景
    useEffect(() => {
      console.log('每次渲染都会执行');
      // 每次渲染都打印当前num(包括初始渲染和所有更新)
      console.log('当前num:', num);
    }); // 无依赖数组:每次渲染都触发
  • 避坑要点

    • 性能隐患:频繁渲染会导致副作用频繁执行,可能引发性能问题(如多次发送请求)。
    • 清理函数会在 "每次重新渲染前" 执行,需确保清理逻辑轻量。

4.三种依赖项对比

依赖项形式 执行时机 典型场景 性能影响
[] 仅挂载 / 卸载 初始化操作 性能最优(只执行一次)
[a, b] 挂载 + 依赖变化时 依赖特定状态的副作用 性能中等(依赖变化时执行)
无依赖 每次渲染 极少场景 性能最差(频繁执行)

5.依赖项的 "浅比较" 规则

React 对依赖数组的变化检测采用 "浅比较":

  • 基本类型(number/string/boolean/null/undefined):比较值是否相等(如1 === 1'a' === 'a')。
  • 引用类型(object/array/function):比较引用地址是否相同(如{a:1}{a:1}因地址不同被视为变化)。

示例

jsx

复制代码
// 错误示例:每次渲染都会创建新对象,导致useEffect频繁执行
useEffect(() => {
  console.log('依赖对象变化了');
}, [{ name: 'react' }]); // 每次渲染都会生成新对象,视为依赖变化

// 正确示例:用useMemo缓存对象,避免不必要的更新
const user = useMemo(() => ({ name: 'react' }), []);
useEffect(() => {
  console.log('依赖对象稳定了');
}, [user]); // 依赖缓存后的对象,仅初始化时执行

五、面试官会问

  1. 为什么 Hooks 只能在函数组件的顶层调用? 答:React 通过 Hooks 的调用顺序来识别和管理状态。如果在条件语句(如if)或循环中调用 Hooks,会导致每次渲染时 Hooks 的调用顺序不一致,React 无法正确关联状态和组件,从而引发错误。
  2. useState 的初始值函数什么时候执行? 答:当初始值需要通过复杂计算得到时,useState( () => initialValue )中的函数仅在组件首次渲染时执行一次,后续渲染会忽略。这是一种性能优化,避免重复计算。
  3. useEffect 的清理函数什么时候执行? 答:有两种情况:① 组件卸载时;② 依赖数组中的变量变化,重新执行effectCallback之前。清理函数的作用是清除上一次副作用产生的 "遗留影响"(如定时器、事件监听),避免内存泄漏。
  4. 如何解决 useEffect 中的闭包问题? 答:闭包问题指effectCallback中获取到的状态是旧值(因为回调函数在创建时捕获了当时的状态)。解决方法:① 将依赖的状态加入依赖数组;② 用useRef存储最新状态(ref.current可获取实时值)。
  5. useState 和 useReducer 的区别? 答:useState适用于简单状态管理(如单个数值、布尔值);useReducer适用于复杂状态逻辑(如状态之间相互依赖、多个子值组成的状态),通过 "action" 来描述状态变化,更易预测和调试。
  6. 为什么依赖数组中加入函数时要小心? 答:函数在每次组件渲染时可能被重新创建(引用变化),导致useEffect误判为依赖变化而频繁执行。解决方法:① 用useCallback缓存函数;② 将函数定义在effectCallback内部(如果仅在副作用中使用)。

六、结语

React Hooks 的出现,不仅简化了组件的写法,更重塑了我们对 React 组件逻辑的组织方式。useState让函数组件拥有了状态管理能力,useEffect让副作用逻辑变得可控可预测,而依赖数组的灵活运用则让副作用的执行时机尽在掌握。

掌握 Hooks 的核心,在于理解 "纯函数" 与 "副作用" 的分离思想 ------ 让组件主体保持纯净,让副作用逻辑可管可控。无论是日常开发中的计数器、表单处理,还是复杂的状态管理、异步请求,Hooks 都能以简洁的代码实现高效的逻辑。

希望通过本文,你能对 React Hooks 有更清晰的认识。在实际开发中,多写多练,结合具体场景灵活运用useStateuseEffect,你会发现函数组件的真正魅力!

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax