React Hooks 设计动机与工作模式|原理篇
30 秒速记
- 核心判断:
Hooks用稳定调用顺序把函数组件的多次执行与内部状态槽位对应起来 - 原理主线:围绕 「React-Hooks 设计动机初探」、「重新理解类组件:包裹在面向对象思想下的“」、「深入理解函数组件:呼应 React 设计」 建立输入、状态变化与输出之间的因果关系
- 文章范围:解析了React Hooks的设计动机、工作原理及其对类组件和函数组件开发模式的影响,帮助开发者理解Hooks如何简化状态管理、复用逻辑和优化组件结构,是学习现代React开发不可错过的原理指南。
- 边界与代价:条件调用会破坏槽位映射,闭包会保留某次渲染快照;严格模式重跑用于暴露副作用问题
- 工程落地:工程上把依赖数组当作数据流声明,优先消除不必要副作用并保证清理可重复执行
Hooks 让函数组件拥有状态和副作用能力,同时依靠稳定的调用顺序把每次渲染与内部状态对应起来。 函数组件每次渲染都是一次新的函数调用,所以回调会捕获当次渲染中的 props 和状态,而不是像类组件那样通过可变的 this 读取最新值。useState 管理单个状态,useEffect 承接订阅、请求和操作 DOM 等副作用。异步回调要留意闭包捕获的旧值,Hook 也不能放进条件分支破坏调用顺序。
这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。
版本校准: 本文若分析 ReactDOM.render、旧生命周期或栈调和,应把它视为理解架构演进的历史路径。React 19 已移除 ReactDOM.render,当前客户端入口使用 createRoot;并发渲染也必须区分可中断的渲染阶段与同步提交阶段。迁移前对照 React 19 官方升级指南。
# React-Hooks 设计动机初探
# 何谓类组件(Class Component)
所谓类组件,就是基于 ES6 Class 这种写法,通过继承 React.Component 得来的 React 组件。以下是一个典型的类组件:
class DemoClass extends React.Component {
// 初始化类组件的 state
state = {
text: ""
};
// 编写生命周期方法 didMount
componentDidMount() {
// 省略业务逻辑
}
// 编写自定义的实例方法
changeText = (newText) => {
// 更新 state
this.setState({
text: newText
});
};
// 编写生命周期方法 render
render() {
return (
<div className="demoClass">
<p>{this.state.text}</p>
<button onClick={this.changeText}>点我修改</button>
</div>
);
}
}
# 何谓函数组件/无状态组件(Function Component/Stateless Component)
函数组件顾名思义,就是以函数的形态存在的 React 组件。早期并没有 React-Hooks 的加持,函数组件内部无法定义和维护 state,因此它还有一个别名叫“无状态组件”。以下是一个典型的函数组件:
function DemoFunction(props) {
const { text } = props
return (
<div className="demoFunction">
<p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
</div>
);
}
# 函数组件与类组件的对比:无关“优劣”,只谈“不同”
我们先基于上面的两个 Demo,从形态上对两种组件做区分。它们之间肉眼可见的区别就包括但不限于:
- 类组件需要继承
class,函数组件不需要; - 类组件可以访问生命周期方法,函数组件不能;
- 类组件中可以获取到实例化后的
this,并基于这个this做各种各样的事情,而函数组件不可以; - 类组件中可以定义并维护 state(状态),而函数组件不可以;
单就我们列出的这几点里面,频繁出现了“类组件可以 xxx,函数组件不可以 xxx”,这是否就意味着类组件比函数组件更好呢?
答案当然是否定的。你可以说,在 React-Hooks 出现之前的世界里,类组件的能力边界明显强于函数组件,但要进一步推导“类组件强于函数组件”,未免显得有些牵强。同理,一些文章中一味鼓吹函数组件轻量优雅上手迅速,不久的将来一定会把类组件干没(类组件:我做错了什么?)之类的,更是不可偏听偏信。
当我们讨论这两种组件形式时,不应怀揣“孰优孰劣”这样的成见,而应该更多地去关注两者的不同,进而把不同的特性与不同的场景做连接,这样才能求得一个全面的、辩证的认知。
# 重新理解类组件:包裹在面向对象思想下的“重装战舰”
类组件是面向对象编程思想的一种表征。面向对象是一个老生常谈的概念了,当我们应用面向对象的时候,总是会有意或无意地做这样两件事情。
- 封装:将一类属性和方法,“聚拢”到一个 Class 里去。
- 继承:新的 Class 可以通过继承现有 Class,实现对某一类属性和方法的复用。
React 类组件也不例外。我们再次审视一下这个典型的类组件 Case:
class DemoClass extends React.Component {
// 初始化类组件的 state
state = {
text: ""
};
// 编写生命周期方法 didMount
componentDidMount() {
// 省略业务逻辑
}
// 编写自定义的实例方法
changeText = (newText) => {
// 更新 state
this.setState({
text: newText
});
};
// 编写生命周期方法 render
render() {
return (
<div className="demoClass">
<p>{this.state.text}</p>
<button onClick={this.changeText}>点我修改</button>
</div>
);
}
}
不难看出,React 类组件内部预置了相当多的“现成的东西”等着你去调度/定制,state 和生命周期就是这些“现成东西”中的典型。要想得到这些东西,难度也不大,你只需要轻轻地继承一个 React.Component 即可。
这种感觉就好像是你不费吹灰之力,就拥有了一辆“重装战舰”,该有的枪炮导弹早已配备整齐,就等你操纵控制台上的一堆开关了。
毋庸置疑,类组件给到开发者的东西是足够多的,但“多”就是“好”吗?其实未必。
把一个人塞进重装战舰里,他就一定能操纵这台战舰吗?如果他没有经过严格的训练,不清楚每一个操作点的内涵,那他极有可能会把炮弹打到友军的营地里去。
React 类组件,也有同样的问题——它提供了多少东西,你就需要学多少东西。假如背不住生命周期,你的组件逻辑顺序大概率会变成一团糟。“大而全”的背后,是不可忽视的学习成本。
再想这样一个场景:假如我现在只是需要打死一只蚊子,而不是打掉一个军队。这时候继续开动重装战舰,是不是正应了那句老话——“可以,但没有必要”。这也是类组件的一个不便,它太重了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的。
更要命的是,由于开发者编写的逻辑在封装后是和组件粘在一起的,这就使得类**组件内部的逻辑难以实现拆分和复用。**如果你想要打破这个僵局,则需要进一步学习更加复杂的设计模式(比如高阶组件、Render Props 等),用更高的学习成本来交换一点点编码的灵活度。
这一切的一切,光是想想就让人头秃。所以说,类组件固然强大, 但它绝非万能。
# 深入理解函数组件:呼应 React 设计思想的“轻巧快艇”
我们再来看这个函数组件的 case:
function DemoFunction(props) {
const { text } = props
return (
<div className="demoFunction">
<p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
</div>
);
}
当然啦,要是你以为函数组件的简单是因为它只能承担渲染这一种任务,那可就太小瞧它了。它同样能够承接相对复杂的交互逻辑,像这样:
function DemoFunction(props) {
const { text } = props
const showAlert = ()=> {
alert(`我接收到的文本是${text}`)
}
return (
<div className="demoFunction">
<p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
<button onClick={showAlert}>点击弹窗</button>
</div>
);
}
相比于类组件,函数组件肉眼可见的特质自然包括轻量、灵活、易于组织和维护、较低的学习成本等。
类组件和函数组件之间,纵有千差万别,但最不能够被我们忽视掉的,是心智模式层面的差异,是面向对象和函数式编程这两套不同的设计思想之间的差异。
说得更具体一点,函数组件更加契合 React 框架的设计理念。何出此言?不要忘了这个赫赫有名的 React 公式:

不夸张地说,React 组件本身的定位就是函数,一个吃进数据、吐出 UI 的函数。作为开发者,我们编写的是声明式的代码,而 React 框架的主要工作,就是及时地把声明式的代码转换为命令式的 DOM 操作,把数据层面的描述映射到用户可见的 UI 变化中去。这就意味着从原则上来讲,React 的数据应该总是紧紧地和渲染绑定在一起的,而类组件做不到这一点。
首先我们来看这样一个类组件:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
这个组件返回的是一个按钮,交互内容也很简单:点击按钮后,过 3s,界面上会弹出“Followed xxx”的文案。类似于我们在微博上点击“关注某人”之后弹出的“已关注”这样的提醒。
看起来好像没啥毛病,但是如果你在这个在线 Demo中尝试点击基于类组件形式编写的 ProfilePage 按钮后 3s 内把用户切换为 Sophie,你就会看到如下图所示的效果:

明明我们是在 Dan 的主页点击的关注,结果却提示了“Followed Sophie”!
这个现象必然让许多人感到困惑:user 的内容是通过 props 下发的,props 作为不可变值,为什么会从 Dan 变成 Sophie 呢?
