React Hooks 是 React 16.8 引入的一套全新 API,极大地提升了函数组件的能力。本文将以 useState
和 useEffect
为核心,在简单demo实践中理解和使用Hooks。
useState:让函数组件拥有"记忆"
函数式编程的力量
在 JavaScript 中,函数是一等对象(First-class Object),意味着函数可以像变量一样被赋值、传递和返回。React 利用这一特性,将函数组件提升为主流。
我们就会思考以下两个问题:
- 既然函数可以像对象一样被操作,能否让它"记住"某些状态?
- 传统的函数每次执行都是"无记忆"的,如何让它有"记忆"?
useState 的魔法
useState
就是让函数组件拥有"记忆"的魔法。它的用法如下:
const [count, setCount] = useState(0);
count
:当前状态值setCount
:更新状态的函数0
:初始值
我们在前端开发服务器中,点击count is 0
按钮count
会自增,其用的就是useState
带来的“魔法”。
所以,我们明白了:
- 每次组件渲染,
useState
都会返回当前的状态值。 - 调用
setCount
会触发组件重新渲染,并用新值替换旧值。
函数也是类?原型式面向对象
在 JS 中,函数不仅是一等对象,还可以作为"类"来使用(通过原型链)。React 的函数组件本质上就是"无状态类",通过 Hooks 赋予其"状态"。
那么,请你思考一下:
- 既然函数可以像类一样使用,能否让它拥有"实例属性"?
- 传统的类组件有
this.state
,函数组件如何实现类似功能?
-
JavaScript 中的函数作为类
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log(`Hello, I'm ${this.name}`); }; const person = new Person("张三"); person.sayHello(); // "Hello, I'm 张三"
-
React 类组件 vs 函数组件
// 类组件 - 有状态 class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } render() { return <div>{this.state.count}</div>; } } // 函数组件 - 原本无状态 function Counter() { return <div>0</div>; // 无法保存状态 }
-
Hooks 让函数组件拥有"类"的能力
// 使用 useState 后,函数组件拥有了"状态" function Counter() { const [count, setCount] = useState(0); return <div>{count}</div>; // 现在可以保存和更新状态了 }
useState
实际上是在函数组件内部创建了一个"隐藏的状态存储",并且每次组件重新渲染时,React 会记住之前的状态值,这就像给函数组件添加了"实例属性",但实现方式更加优雅和函数式。
useEffect:副作用的管理者
什么是副作用(Side Effect)?
副作用指的是任何与组件渲染无关、但又需要在组件生命周期内执行的操作,比如:
- 数据请求
- 订阅/取消订阅
- 手动操作 DOM
- 定时器
useEffect 的基本用法
useEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑
};
}, [依赖项]);
- 第一个参数是副作用函数
- 返回值是清理函数(可选)
- 第二个参数是依赖项数组
生命周期的映射
生命周期阶段 | useEffect 写法 | 说明 |
---|---|---|
挂载(mounted) | useEffect(fn, []) | 只在初次渲染后执行一次 |
更新(updated) | useEffect(fn, [依赖]) | 依赖项变化时执行 |
卸载(unmounted) | return () => { ... } | 组件卸载时执行清理逻辑 |
你再思考以下问题:
为什么要清理副作用?
答:比如定时器、订阅等,如果不清理会造成内存泄漏。而且请求数据时,组件卸载后响应式数据和 DOM 已经不存在,继续处理响应会报错或浪费资源。
代码示例
demo 项目中有如下代码:
import { useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// 清理副作用
return () => clearInterval(timer);
}, []);
return <div>计时:{count} 秒</div>;
}
跟着我一起重复一遍:
组件挂载时启动定时器,每秒自增
组件卸载时清理定时器,防止内存泄漏
组件请求接口的最佳时机
何时请求数据?
- 组件首次 渲染后(mounted) 是请求数据的最佳时机
- 使用
useEffect(fn, [])
,只在初次渲染后执行一次
为什么不用依赖项?
- 如果依赖项为空数组
[]
,组件状态改变不会重复请求 - 只有在依赖项变化时才会重新执行副作用
请参考以下代码:
import { useState, useEffect } from 'react';
function App() {
const [repos, setRepos] = useState([]);
useEffect(() => {
// api 数据 动态的
console.log('只在组件挂载时运行一次!!');
const fetchRepos = async () => {
const response = await fetch('https://api.github.com/users/xxxx/repos');
const data = await response.json();
console.log(data);
setRepos(data);
}
fetchRepos();
}, []);
return (
<div>
{/* 渲染仓库列表 */}
</div>
);
}
组件首次渲染时,useEffect
执行,发起 API 请求,依赖项为空数组 []
,确保了只在组件挂载时执行一次。即使组件状态改变导致重新渲染,也不会重复请求数据,这是数据请求的最佳实践:只在需要时请求一次。
useEffect 不能直接 async 的原因
❌ 错误写法:直接使用 async
// 错误!useEffect 不能直接是 async 函数
useEffect(async () => {
const response = await fetch('https://api.github.com/users/LeeAt67/repos');
const data = await response.json();
setRepos(data);
}, []);
错误写法的报错提醒
✅ 正确写法:在内部声明 async 函数
useEffect(() => {
async function fetchData() {
// 异步请求
}
fetchData();
}, []);
为什么不能直接 async?
useEffect
期望返回一个清理函数(或 undefined),而 async 函数返回的是 Promise,这会导致 React 无法正确处理副作用的清理,这会导致控制台警告和潜在的清理逻辑失效。
总结:让我们再次回顾一下!!!!
- Hooks 让函数组件拥有了"记忆"和"副作用管理"能力
useState
负责状态,"记住"数据useEffect
负责副作用和生命周期- 清理副作用是防止内存泄漏的关键
- 请求数据应在组件首次渲染后进行
useEffect
内不能直接 async,需包裹一层