前言:为什么要关注这次变革
刚学 React 那会儿,我最大的困惑不是JSX,也不是虚拟DOM,而是:为什么写个组件要这么多“仪式感”? class、constructor、this.state、bind(this) 一套下来,总有种“只是想点个按钮,却要先填三张表”的感觉。后来 Hooks 出现,我才意识到,这并不是我水平不够,而是 React 自己也在进化。
一、类组件时代:结构与痛点
在 Hooks 出现之前,组件几乎等同于 class。你要状态、要副作用、要生命周期,全都得写在一个类里,这种写法本身没错,但对初学者并不友好。
1.1 状态管理:this.state 与 setState
在类组件里,如果一个变量希望“改了就刷新页面”,那它就不能是普通变量,必须老老实实放进 this.state,并通过 setState 修改。
export default class App extends Component {
constructor() {
super()
this.state = { count: 0 }
}
add() {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<button onClick={this.add.bind(this)}>
{this.state.count}
</button>
)
}
}
这套机制本身是严谨的,但问题在于:
你在还没真正理解“状态是什么”之前,就被迫理解了 this、bind 和 class。
1.2 生命周期:分散的副作用管理
类组件的生命周期,本质上是在描述组件的一生,最常用的无非这三个:
- componentDidMount:组件第一次渲染完成,适合发请求、开定时器
- componentDidUpdate:状态或 props 更新后触发,适合同步变化
- componentWillUnmount:组件卸载前,清理定时器、解绑事件
import React, { Component } from 'react';
export default class App3 extends Component {
constructor(props) {
super(props);
this.state = {
list: [],
count: 0,
};
this.timer = null;
this.controller = new AbortController();
}
// 组件挂载完成:请求数据 + 初始化副作用
componentDidMount() {
fetch('https://mock-api.com/work/list', {
signal: this.controller.signal,
})
.then(res => res.json())
.then(data => {
const list = data?.list || [];
this.setState({ list, count: list.length });
});
this.timer = setInterval(() => {
console.log('count:', this.state.count);
}, 3600000);
}
// 组件更新完成:基于变化执行副作用
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
console.log('count changed:', this.state.count);
}
}
// 组件卸载前:清理副作用
componentWillUnmount() {
clearInterval(this.timer);
this.controller.abort();
}
add = () => {
this.setState(state => ({ count: state.count + 1 }));
};
render() {
const { list, count } = this.state;
return (
<div>
<p>count: {count}</p>
<button onClick={this.add}>add</button>
<ul>
{list.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
</div>
);
}
}
componentDidMount:组件第一次挂载时执行,这里用于发起 fetch 拿 list 并用 setState 更新,同时启动 setInterval 定时任务;如果请求可能被中断,应保存 AbortController 以便后来取消。
componentDidUpdate:组件每次更新后都会触发,代码中只在 count(或 todoCount)真实变化时才执行后续操作,目的是避免无条件 setState 导致的无限循环。
componentWillUnmount:组件卸载前执行清理,包括 clearInterval 停掉定时器和用 abort() 取消未完成的网络请求,防止卸载后继续访问已销毁的组件状态或造成内存泄漏。
初始化、响应变化、清理三部分职责清晰,但问题也正出在这里:同一件事的“开始、变化、结束”被拆散在不同方法里,维护成本大大增加。
刚学时的我就很头疼了——逻辑是懂的,但就是不知道该去哪一段代码。
二、函数组件与 Hooks:新的表达方式
Hooks 出现后,函数组件不再只是“展示组件”,而是正式拥有了状态和副作用的能力,而且写法直观得多。
2.1 useState:把状态交给 React 管理
很多人第一次写函数组件都会踩这个坑:
export default function App() {
let count = 0
function add() {
count++
}
return <button onClick={add}>{count}</button>
}
不管点多少次,页面永远是 0。
原因其实很简单:变量的值虽然变了,但 React 并不知道这次变化意味着组件需要重新渲染。
这时 useState 才真正登场。
import { useState } from 'react'
export default function App() {
const [count, setCount] = useState(0)
function add() {
setCount(count + 1)
}
return <button onClick={add}>{count}</button>
}
你可以把 useState 理解成一句话:
“这个值我交给 React 管,你帮我记住,也帮我决定什么时候重渲染。”
2.2 useEffect:按依赖聚合副作用(含依赖数组作用简述)
在类组件里,你要分别记住三个生命周期;
在 Hooks 里,useEffect 用“依赖关系”统一解决了这些问题。
import { useEffect, useState } from 'react';
export default function App2() {
const [list, setList] = useState([]);
const [count, setCount] = useState(0);
// 只在组件首次渲染后执行:用于初始化数据
useEffect(() => {
fetch('https://mock.mengxuegu.com/mock/66585c4db462b81cb3916d3e/songer/songer')
.then(res => res.json())
.then(data => {
setList(data.data || []);
});
}, []);
// 依赖 count:只有 count 发生变化才会执行
useEffect(() => {
console.log('count 发生变化:', count);
}, [count]);
// 创建副作用,并在组件卸载前清理
useEffect(() => {
const timer = setInterval(() => {
console.log('timer running');
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return (
<div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
<ul>
{list.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
useEffect 的第二个参数 [中括号内] 用来声明这个副作用依赖哪些状态。
当传入 [] 时,表示不依赖任何状态,只在组件首次渲染后执行一次;当传入 [count] 时,表示只有 count 发生变化,这个副作用才会再次执行。
换句话说,依赖数组决定什么时候该重新跑这段逻辑。
useEffect 并不是简单地“替代生命周期”,而是通过依赖数组明确告诉 React:在什么条件下执行副作用,以及什么时候需要清理它。初始化、响应变化和收尾逻辑不再被强制拆散,而是按“关注的条件”自然聚合。
三、演进价值:Hooks 带来的改进
在类组件中,组件必须写成 class,状态集中放在 this.state 里,事件方法还需要手动处理 this 的指向。代码虽然规范,但样板代码偏多,真正的业务逻辑往往被这些结构性写法包裹住,读代码时需要先适应“类组件的语法规则”。
Hooks 出现之后,组件可以直接写成普通函数,状态通过 useState 拆分成一个个独立的变量,哪里用、哪里声明,逻辑关系更加直观。事件函数不再需要关心 this,组件本身更像是在描述“当前状态下页面应该长什么样”。
在副作用处理上,类组件需要把相关逻辑分散到不同的生命周期中,而 Hooks 通过 useEffect 按依赖条件聚合逻辑,让“因为什么变化、执行什么副作用”变得一眼可读,也更容易抽离和复用。
整体来看,Hooks 并不是让代码“更短”,而是减少了思考路径:你不再需要在多个生命周期之间来回跳转,而是围绕状态和依赖去理解组件行为,心智负担自然就降了下来。
最后
React 从类组件走向 Hooks,并不是“推翻重来”,而是一次工程体验上的优化。类组件帮我们理解了状态、生命周期和副作用的直观认知, Hooks 则让这些概念用更直观的方式表达出来。
如果你现在还在学类组件,完全不用焦虑;
等你理解 Hooks,再回头看 class,你反而会觉得:
原来当年的复杂,是有原因的。
这篇文章是我在学习过程中的一次阶段性总结,如果哪里理解有偏差,也欢迎指出。React 的路还长,慢慢走,总会走顺。