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,你会发现函数组件的真正魅力!

相关推荐
Alair‎1 小时前
300TypeScript基础知识
javascript
旧梦吟2 小时前
脚本网页 三人四字棋
前端·数据库·算法·css3·html5
莫物2 小时前
element el-table表格 添加唯一标识
前端·javascript·vue.js
我看刑2 小时前
【已解决】el-table 前端分页多选、跨页全选等
前端·vue·element
我会一直在的2 小时前
Fiddler基础使用介绍
前端·测试工具·fiddler
小明记账簿2 小时前
前端文件流下载方法封装
前端
IT_陈寒2 小时前
Vite 5大优化技巧:让你的构建速度飙升50%,开发者都在偷偷用!
前端·人工智能·后端
CodeCraft Studio2 小时前
Vaadin 25 正式发布:回归标准Java Web,让企业级开发更简单、更高效
java·开发语言·前端·vaadin·java web 框架·纯java前端框架·企业级java ui框架
Shirley~~2 小时前
PPTist 幻灯片工具栏Toolbar部分
开发语言·前端·javascript