深度驱动: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 + 清理函数 能覆盖全部阶段。

相关推荐
flashlight_hi2 小时前
LeetCode 分类刷题:199. 二叉树的右视图
javascript·算法·leetcode
刘一说2 小时前
Vue Router:官方路由解决方案解析
前端·javascript·vue.js
OpenTiny社区2 小时前
🎉 TinySearchBox 重磅更新:支持 Vue2,一次满足我的所有需求!
前端·javascript·vue.js
@大迁世界2 小时前
面了 100+ 次前端后,我被一个 React 问题当场“打回原形”
前端·javascript·react.js·前端框架·ecmascript
小六*^____^*3 小时前
虚拟列表学习
前端·javascript·学习
1024肥宅3 小时前
工程化工具类:实现高效的工具函数库
前端·javascript·面试
骑驴看星星a3 小时前
【回顾React的一些小细节】render里不可包含的东西
前端·javascript·react.js
妮妮喔妮3 小时前
Nextjs的SSR服务器端渲染为什么优化了首屏加载速度?
开发语言·前端·javascript