深度驱动:React Hooks 核心之 `useState` 与 `useEffect` 实战详解

在 React 的函数组件世界里,Hooks 是灵魂所在。正如你的 readme.md 中所言,"以 use 开头的函数,都是 React 提供的 Hooks,用于在函数组件中使用状态和其他 React 功能。"其中,useState 解决了"数据如何驱动视图"的问题,而 useEffect 则解决了"如何处理纯函数之外的副作用"的问题。

第一部分:useState------赋予组件"记忆"的能力

1. 响应式状态的本质

在 React 之前,普通变量(如 let count = 0)的变化无法触发 UI 的重新渲染。useState 的出现,为程序员带来了关键的响应式状态状态就是变化的数据,组件的核心是状态。

最典型的定义方式:

JavaScript 复制代码
const [count, setCount] = useState(1);
  • 数组解构useState 返回一个长度为 2 的数组。第一项是当前状态的值,第二项是更新该状态的函数。
  • 确定性:初始化时,状态必须是确定的,肯定的。

2. 惰性初始化(Lazy Initialization)

当你需要通过复杂计算来确定初始值时,可以向 useState 传入一个纯函数

什么是纯函数?

纯函数是指对于相同的输入始终返回相同输出,且没有副作用(如修改外部状态或依赖外部可变数据)的函数。

JavaScript 复制代码
const [count, setCount] = useState(() => {
  const num1 = 1 + 2;
  const num2 = 2 + 3;
  return num1 + num2;
});

为什么要用函数?

如果初始值计算开销很大(比如读取本地缓存、循环计算),直接写在 useState(expensive()) 中会导致每次组件重新渲染时都执行该计算。而传入函数,React 只会在组件初次挂载时执行它一次。

注意,这个函数必须是同步的,不支持异步的,异步的初始会带来不确定性,而状态必须是可预测且确定的。

3. 更新函数的两种姿势

更新状态的两种逻辑:

  1. 直接传递新值setCount(count + 1)
  2. 传递更新函数setNum(prevNum => prevNum + 1)

核心区别:

使用 prevNum => prevNum + 1 是为了安全地基于上一次的值更新。由于 React 的状态更新可能是异步的,或者在闭包环境中,直接引用 count 可能会拿到旧值。使用回调函数形式可以确保你拿到的 prevNum 永远是内存中最新的状态。

第二部分:useEffect------掌控副作用的艺术

1. 什么是副作用(Side Effect)?

副作用的对立面就是纯函数。

  • 纯函数:相同输入永远得到相同输出,无副作用。
  • 副作用:指那些不直接参与 UI 计算的操作,如网络请求、手动修改 DOM、设置定时器、记录日志等。

useEffect 的设计初衷,就是为这些"不纯"的操作提供一个安全的避风港。

2. 依赖项数组(Dependency Array)的奥秘

useEffect 的第二个参数决定了副作用何时运行,这是初学者最容易困惑的地方:

依赖参数 执行时机 对应生命周期
不传 每次组件渲染(render)后都执行 持续更新
空数组 [] 仅在组件挂载(Mount)后执行一次 onMounted
有值 [num] 挂载后执行,且当 num 变化时重新执行 onUpdated

3. 清理函数(Cleanup):防止内存泄漏

这是 App.jsxDemo.jsx 中最精彩的部分。

JavaScript 复制代码
useEffect(() => {
    const timer = setInterval(() => { console.log(num); }, 1000);
    return () => {
        clearInterval(timer); // 清理函数
    }
}, [num]);

为什么要 Cleanup?

  1. 重复创建问题 :如果不清除,每次 num 变化都会产生一个新的定时器,旧的定时器依然在后台运行。
  2. 内存泄漏 :在 Demo.jsx 中,如果组件卸载了(num 变成奇数),但定时器没关,它会尝试操作已经不存在的逻辑,导致性能下降甚至崩溃。

执行时机

  1. 清理函数会在下一次副作用执行前 被调用,如App.jsx中加的[num]依赖,在num发生改变时,副作用重新执行。

  2. 清理函数会在组件卸载前 被调用,如Demo.jsx中的[]依赖,副作用仅在组件挂载后执行一次,但在num为奇数时,Demo子组件会卸载。

第三部分:实战代码深度剖析

案例 1:App.jsx 中的动态监听

App.jsx 中,useEffect 监听了 [num]。当用户点击 div 使 num 增加时:

  1. React 发现 num 变了。

  2. React 先调用上一次 Effect 返回的 remove 清理函数,执行 clearInterval

  3. React 执行新的 Effect,开启一个新的定时器,打印最新的 num。

    这就是为什么你在控制台能看到"实时更新"的数字,且不会堆积多个定时器。

案例 2:Demo.jsx 的挂载与卸载

JavaScript 复制代码
useEffect(() => {
    console.log('123123'); // 仅在 Demo 出现时打印一次
    return () => { console.log('remove'); } // 仅在 Demo 消失时执行
}, []);

在主组件中:{num % 2 === 0 && <Demo />}

  • num 从 0 变 1 时,条件为假,Demo 组件被销毁。
  • 此时 React 自动触发 Demo 内部 useEffectreturn 函数。
  • 结论 :这完美模拟了 Vue 中的 onMountedonUnmounted

第四部分:Hooks 的使用准则

为了确保这些钩子正常工作,必须遵守 React 的两条金科玉律:

  1. 只在最顶层使用 Hooks:不要在循环、条件判断或嵌套函数中调用 Hook。这保证了每次渲染时 Hook 的调用顺序一致,React 才能正确关联状态。
  2. 只在 React 函数中调用 Hooks :不要在普通的 JS 函数中调用,除非是你自定义的 Hook(以 use 开头)。

第五部分:总结与感悟

useStateuseEffect 的组合,体现了 React 声明式编程的思想:

  • 你只需要声明: "当数据是这样时,界面应该长这样"useState)。
  • 你只需要声明: "当数据变化时,我需要同步做这些额外的事"useEffect)。

通过提供清理机制,React 强迫开发者思考资源的生命周期,从而编写出更健壮、无泄漏的前端应用。

第六部分:App.jsx和Demo.jsx源码

这个 App.jsx + Demo.jsx 应用主要做了两件事:

  1. 点击一个数字,让它加 1。
  2. 当数字是偶数时,显示一个叫 的组件;奇数时隐藏它。
  3. 同时,用定时器每隔 1 秒打印当前数字,并确保不会造成内存泄漏(比如旧的定时器没关掉)。
js 复制代码
// App.jsx
import { useState, useEffect } from 'react';
import Demo from './components/Demo.jsx';

async function queryData() {
    const data = await new Promise(resolve => {
        setTimeout(() => {
            resolve(666);
        }, 2000)
    });
    return data;
}

export default function App() {
    const [num, setNum] = useState(0);
    useEffect(() => {
        const timer = setInterval(() => {
            console.log(num);
        }, 1000)
        return () => {
            console.log('remove');
            clearInterval(timer);
        }
    }, [num])
    return (
        <>
            <div onClick={() => setNum(prevNum => prevNum + 1)}>{num}</div>
            {num % 2 === 0 && <Demo />}
        </>
    )
}

主组件 App

每当num发生变化时,这个 useEffect 会先清理上一次的副作用(通过return函数),再执行新的副作用。

它启动了一个每秒打印当前 num 的定时器。

返回的函数(return () => {...})就是清理函数,会在下一次 useEffect 执行前,或者组件卸载时自动调用。

为什么需要清理?

如果不清理,每次 num 变化都会新建一个定时器,但旧的还在跑! 比如:num=0 时开了一个定时器,num=1 时又开一个......最后可能有 10 个定时器同时打印,造成内存泄漏或逻辑混乱。

所以:只要用了 setInterval、setTimeout、监听事件等,几乎都要写清理函数。

js 复制代码
import {useEffect, useState} from 'react';

export default function Demo() {
    useEffect(() => {
        const timer = setInterval(() => {
            console.log('timer');
        }, 1000)
        // 当 num 是偶数时,子组件还在页面上,没有卸载 React 不会执行return清理函数
        // 而当 num 变为奇数时,<Demo /> 被移除 → React 自动调用 return () => {...} → 清理定时器。
        return () => { 
            console.log('remove');
            clearInterval(timer);
        }
    }, [])
    return (
        <div>
            偶数Demo
        </div>
    )
}

子组件 Demo

useEffect(..., \[\]):只在组件第一次挂载时执行一次(类似 Vue 的 onMounted)。

当 num 变成奇数时, 被移除 → React 自动调用 return () => {...} → 清理定时器。

这样就避免了:组件都消失了,定时器还在后台跑!

这就是 React 的"生命周期"思想:挂载 onMounted → 更新 onUpdated→ 卸载 onUnmounted,而 useEffect + 清理函数 能覆盖全部阶段。

相关推荐
文阿花12 分钟前
Echarts实现自定旋转3D饼状图
javascript·3d·echarts·饼状图
meilindehuzi_a1 小时前
深入理解 JavaScript 的同步与异步机制:从单线程设计到 Promise 核心应用
开发语言·javascript·ecmascript
如烟花的信页1 小时前
加速乐cookie逆向分析
javascript·爬虫·python·js逆向
永远的WEB小白1 小时前
css改变svg图标的颜色
前端·javascript·css
ikoala1 小时前
Codex 不得不装的 12 个插件,都在这了
前端·javascript·后端
赵庆明老师2 小时前
JS检查提交的文件是否合规
开发语言·前端·javascript
颂love2 小时前
Vue的两大生态以及组件通信
前端·javascript·vue.js·typescript
光影少年2 小时前
js单线程,为什在node环境下的js可以处理高并发请求?
前端·javascript·掘金·金石计划
vim怎么退出3 小时前
Dive into React——事件系统
前端·react.js·源码阅读
moMo3 小时前
# JavaScript 的“等等我”:聊聊同步与异步
javascript