友情提示:阅读本文时请确保你的大脑内存未被 useState 占用,否则可能导致 useEffect 依赖项紊乱。
第一章:初识魔法棒——useState ✨
想象一下,你是个函数组件世界的魔法学徒,而 useState
就是你的第一根魔法棒。它能让你在函数组件中召唤状态精灵:
useState是什么呢?
useState
是 React 中的一个 Hook,它使得函数组件可以拥有自己的状态(state)。在使用类组件时,我们通常需要通过 this.state
和 this.setState
来管理组件的状态。而在函数组件中,由于没有实例(即没有 this
),因此引入了 Hooks 的概念来解决这个问题,useState
就是其中之一。
基本用法
import React, { useState } from 'react';
function Example() {
// 声明一个新的叫做 “count” 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
在这个例子中,useState(0)
创建了一个名为 count
的状态变量,并将其初始值设置为 0
。setCount
函数用于更新这个状态。这时候我们数据发生改变,我们页面的数据也会被驱动改变,和原生JS不同,我们不需要去操作DOM
优点
- 简化状态管理:无需编写冗长的类构造函数或使用
this
关键字,让代码更加简洁。 - 增强可读性与维护性:每个
useState
调用都是独立且清晰的,易于理解和维护。 - 更灵活的状态逻辑:允许你在不改变组件层次结构的情况下复用状态逻辑,特别是在结合其他 Hooks(如
useEffect
)时。 - 更好的性能优化:React 对函数组件和 Hooks 进行了优化,能够更好地处理组件的重新渲染,提高应用性能。
- 促进模块化设计:可以使功能更模块化、更可重用,有助于构建大型应用程序。
第二章:时空管理者——useEffect 🕰️
如果说 useState 是魔法棒,那么 useEffect
就是你的时间管理器。它专门处理那些"不该在渲染时捣乱"的副作用任务。
React 官方文档中将副作用定义为:
“任何在渲染之后需要执行的操作。”
你可以把它理解为类组件生命周期方法的组合替代
2.1 基础生存法则
useEffect(() => {
console.log('我像幽灵一样每次渲染后都出现')
}) // 没有依赖项 = 阴魂不散
一但页面重新渲染,就会执行,比如我们上诉用useState
改变页面的数据
2.2 挂载模式:[] 依赖项
useEffect(() => {
console.log('只在组件挂载时运行一次!!!')
fetchRepos() // 趁现在赶紧偷数据!
}, []) // 空数组 = 仅出生时执行
这就像给新生儿办出生证明——只执行一次的重要仪式。只在页面刚刚渲染完成时执行
2.3 追踪模式:监视特定状态
useEffect(() => {
console.log('count变了!快通知监控中心!')
}, [count]) // 监视count精灵的异动
这相当于给你的状态精灵安装了 GPS 追踪器。一但count改变,就会执行useEffect里面的逻辑,当然我们也可以同时监视多个数据
useEffect 的优点
优点 | 说明 |
---|---|
✅ 统一副作用管理 | 将所有副作用集中处理,避免分散在多个生命周期中,提高可维护性。 |
✅ 组件解耦更清晰 | 不再需要在多个生命周期中来回跳转,副作用逻辑更容易理解和测试。 |
✅ 自动处理依赖更新 | 通过依赖数组 [deps] 控制执行时机,避免不必要的重复调用。 |
✅ 支持清理机制 | 可以返回一个函数用于清理副作用,防止内存泄漏。 |
✅ 复用逻辑更方便 | 可以封装成自定义 Hook,在多个组件之间共享副作用逻辑。 |
第三章:生命周期大逃杀 ♻️
类组件的生命周期在函数组件中化身三个 useEffect 形态:
生命周期 | useEffect 形态 | 典型用途 |
---|---|---|
Mounted | useEffect(fn, []) | 初始化数据、开启定时器 |
Updated | useEffect(fn, [dep]) | 依赖项变化时更新 |
Unmount | return cleanup | 清理定时器、取消请求 |
在 Timer 组件中,我们完美展示了这个循环:
useEffect(() => {
const interval = setInterval(() => {
setTime(prev => prev + 1) // 定时器:时间刺客
}, 1000)
return () => { // 卸载时的清理小队
clearInterval(interval) // 消灭时间刺客!
console.log('组件卸载')
}
}, []) // 出生时召唤刺客,死亡时消灭刺客
我们设置了一个定时器,也就是查看页面运行多久,当我们在根组件,修改这个Timer状态
{isTimerOn && <Timer />}
<button onClick={() => {
setTisTimerOn(!isTimerOn)
}}>toogle timer</button>
这样我们的根组件就重新渲染,就会执行我们的useEffect,卸载组件,否则还是会继续执行
第四章:API 请求的黄金时机 ⏳
组件世界最重要的哲学问题:何时请求数据?
答案藏在 根 组件的这个片段:
useEffect(() => {
const fetchRepos = async () => {
const response = await fetch('https://api.github.com/...')
setRepos(await response.json()) // 偷到数据塞给仓库精灵
}
fetchRepos()
}, []) // 挂载时偷一次就够了
这里我们直接在页面渲染完成后就执行API的请求,而且由于是[]
依赖项,我们只在初次组件渲染请求,很好的满足了我们的要求
为什么是 useEffect 而不是直接放在函数体里?
- 避免阻塞渲染:请求是异步操作,不挡路
- 避免重复请求:依赖项 [] 保证只偷一次
- 避免内存泄漏:配合清理函数更安全
还有为什么async不能写在useEffect这个函数上呢?
错误原因:
useEffect
要求传入的函数要么返回一个清理函数(function),要么返回undefined
。- 如果你把
useEffect
的回调写成async
函数,它会返回一个Promise
,而不是undefined
或函数。 - React 不知道如何处理这个 Promise,所以会警告或导致副作用行为异常。
第六章:精灵动物园实战 🦁
看看 App 组件如何管理多个状态精灵:
function App() {
// 精灵召唤仪式
const [count, setCount] = useState(0)
const [num, setNumber] = useState(0)
const [repos, setRepos] = useState([])
const [isTimerOn, setTisTimerOn] = useState(true)
// 监视精灵们的特殊效果
useEffect(() => console.log('count变啦!'), [count])
useEffect(() => console.log('num变啦!'), [num])
useEffect(() => console.log('有精灵动了!'), [count, num])
// 组件卸载时的彩蛋
return (
<>
{isTimerOn && <Timer />}
<button onClick={() => setTisTimerOn(!isTimerOn)}>
{isTimerOn ? '封印计时器' : '解封计时器'}
</button>
</>
)
}
点击按钮时触发计时器组件的生与死:
// Timer被卸载时执行清理函数
return () => {
console.log('组件卸载')
clearInterval(interval) // 必须清理!
}
第七章:Hooks 生存法则 📜
- 精灵召唤法则:只能在函数组件顶部调用 Hook
- 依赖诚实原则:useEffect 依赖项必须诚实交代所有用到的状态
- 清理义务:有开启必有清理(定时器、订阅等)
- 异步隔离原则:async 函数必须在 useEffect 内部定义执行
- 精灵独立宣言:每个状态都有独立存储空间
结语:函数组件的重生 🦋
当 Timer 组件中的 console.log('组件函数执行')
每次渲染都被调用时,useEffect 里的逻辑却像经过精密设计的定时炸弹——只在正确时机引爆。这种"函数体执行 ≠ 副作用执行"的特性,正是 Hooks 的精妙之处。
最后赠送 Hook 修炼心法口诀:
状态用 useState,
副作用靠 Effect。
依赖数组要诚实,
清理函数不能缺。
Async 包在内部用,
顶层调用是铁则!