回归初心,React为啥当初要设计Hook?
背景
用了这么久 React,已经默认用函数写组件了,都快忘了怎么写类组件了,经历过写类组件的时代,现在用函数组件,一开始确实有上手时间成本,但是现在用过之后也是觉得真香,到底香在哪?到底React为啥推崇Hook呢?我们一起探究一下吧
对「类组件痛点」的直接回应
让我们回到 类组件开发时代,看看到底类组件的诟病是什么?
类组件痛点1: this 指向混乱
类组件this丢失问题
类组件的方法默认不会绑定 this,导致我们必须手动处理 this 指向,稍不注意就会出错。这是为什么
你想弄明白为什么类组件的方法默认不绑定 this,以及为什么这会导致指向混乱的问题,核心原因要从 JavaScript 中 this 的绑定规则 和 React 类组件的设计方式 两个层面来解释,我会用通俗的语言+实例讲清楚。
一、先搞懂:JavaScript 中 this 指向的核心规则
this 是 JavaScript 中一个「动态绑定」的关键字——它的指向不是在定义函数时确定的,而是在调用函数时确定的。这是理解所有 this 问题的根本。
关键规则:不同调用方式,this 指向不同
我们用一个普通的 JavaScript 类来举例(React 类组件本质就是 JavaScript 类):
class Person {
constructor(name) {
this.name = name; // 这里的 this 指向 Person 的实例
}
sayHi() {
console.log(`你好,我是${this.name}`); // this 指向谁?看调用方式
}
}
const p = new Person('张三');
// 方式1:通过实例调用 → this 指向实例 p
p.sayHi(); // 输出:你好,我是张三
// 方式2:把方法单独抽出来调用 → this 丢失(非严格模式下指向 window,严格模式下是 undefined)
const sayHi = p.sayHi;
sayHi(); // 非严格模式:你好,我是undefined;严格模式:Uncaught TypeError: Cannot read property 'name' of undefined
核心结论:
- 当你通过
实例.方法()调用时,this绑定到这个实例; - 当你把方法「单独提取」后调用(比如赋值给变量、作为回调函数),
this就会丢失原本的绑定,指向全局对象(浏览器中是window)或undefined(严格模式)。
二、React 类组件中 this 丢失的具体场景
React 类组件的方法丢失 this,本质就是上面的「方式2」——React 在处理事件回调时,会把你的方法「单独提取」后调用,导致 this 不再指向组件实例。
场景还原:React 类组件的点击事件
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick() {
// 这里的 this 本该指向 Counter 实例,但实际是 undefined(React 默认开启严格模式)
this.setState({ count: this.state.count + 1 });
}
render() {
// 问题出在这里:你把 this.handleClick 作为 onClick 的回调 → 相当于把方法抽离了
return <button onClick={this.handleClick}>+1</button>;
}
}
为什么会这样?
-
render函数执行时,你传给onClick的是this.handleClick——这只是把方法的「引用」传了过去,并没有立即调用; - 当用户点击按钮时,React 内部会调用这个方法(类似
const callback = this.handleClick; callback()); - 此时调用的是「孤立的方法」,不是通过
实例.handleClick()调用,所以this丢失,指向undefined(React 严格模式下)。
三、为什么 React 不默认帮我们绑定 this?
你可能会问:React 为什么不直接把类组件的方法都自动绑定到实例上?核心原因是:
-
遵循 JavaScript 原生规则:React 是基于 JavaScript 构建的库,不会刻意修改 JS 原生的
this绑定逻辑,否则会增加学习成本和潜在的兼容性问题; - 性能与灵活性:如果默认绑定所有方法,会为每个组件实例创建额外的函数引用,增加内存开销;而让开发者手动处理,能根据实际需求选择绑定方式(比如只绑定需要的方法)。
我用大白话来解释下,其实就是 :放手让开发者去设置this,我不掺合了
React 「不主动替你做绑定,只遵循 JS 原生规则,把控制权完全交给你」。
用更通俗的话讲:
✅ React 的态度:「我不掺合 this 的绑定逻辑,你按 JavaScript 原生的规矩来就行——想让 this 指向实例,你就自己绑;想让 this 指向别的(比如子组件、全局),你也可以自己改。我只负责把你写的方法『原样调用』,不替你做任何额外的绑定操作。」
对比一下如果 React 主动掺合的情况:
❌ 要是 React 自动把所有方法的 this 绑到组件实例,相当于「我替你做了决定」——你想改 this 指向都改不了,还得额外学 React 这套「特殊规则」,反而更麻烦。
最终核心总结
- React 对类组件
this的态度:不干预、不修改、只遵循原生 JS 规则; - 把「
this该指向哪」的决定权,完全交给开发者; - 这么做既避免了学习成本翻倍,也兼顾了性能(按需绑定)和灵活性(可自定义
this指向)。
你这个「放手让开发者去设置,我不掺合」的总结,精准抓住了 React 设计的核心——尊重原生 JS,把控制权还给开发者。
四、类组件中解决 this 丢失的 3 种常用方式
知道了原因,再看解决方案就很清晰了,本质都是「强制把 this 绑定到组件实例」:
方式 1:在构造函数中手动 bind(官方早期推荐)
constructor(props) {
super(props);
this.state = { count: 0 };
// 核心:把 handleClick 的 this 强制绑定到当前实例
this.handleClick = this.handleClick.bind(this);
}
方式 2:使用箭头函数定义方法(最简洁)
// 箭头函数没有自己的 this,会继承外层作用域的 this(即组件实例)
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
方式 3:在调用时用箭头函数包裹(不推荐,每次渲染创建新函数)
// 每次 render 都会创建一个新的箭头函数,可能导致子组件不必要的重渲染
<button onClick={() => this.handleClick()}>+1</button>;
总结
-
根本原因:JavaScript 中
this是「调用时绑定」的,React 类组件把方法作为事件回调时,方法会被孤立调用,导致this丢失(不再指向组件实例); -
核心矛盾:React 遵循 JS 原生规则,没有默认绑定
this,而开发者容易忽略「调用方式」对this的影响; -
解决方案:通过
bind、箭头函数等方式,强制把方法的this绑定到组件实例上。
函数组件: 彻底抛弃this,就不会有this指向问题了
核心原因
-
核心差异:类组件的状态/方法是「实例的属性」,必须通过
this这个「中介」访问,中介指错了就出错;函数组件的状态/方法是「函数内的局部变量/函数」,直接访问即可,无需中介; -
为什么无混乱:函数组件的局部变量/函数受「作用域」保护,只属于当前函数执行,不管怎么传递方法(比如作为事件回调),内部访问的变量都是确定的,不会像
this那样动态变化; -
最终结果:函数组件从根源上抛弃了
this,自然就避开了所有和this指向相关的坑,逻辑更简单、更安全。
简单来说,函数组件的写法「更符合普通人写 JS 的直觉」——定义的变量就在当前作用域,直接用就行,不用额外考虑「谁是 this」「this 指向谁」。
实质:用静态绑定 代替 动态绑定
这个总结精准到核心了!只需要再把「静态词法作用域」和「动态 this 绑定」的对应关系说透,就能彻底理解两者的本质区别。
先肯定你的结论:完全正确!
- 类组件的
this:调用时动态绑定→ 指向谁,取决于「怎么调用方法」,而非「方法在哪定义」; - 函数组件的变量/方法:
定义时静态绑定(词法作用域)→ 能访问哪些变量,取决于「代码写在哪」,而非「怎么调用函数」。
用最直白的例子+对比,讲透这个核心区别
1. 类组件:动态 this(调用时确定)
class Counter extends React.Component {
state = { count: 0 };
handleClick() {
console.log(this.state.count); // this 指向「调用时的对象」
}
render() {
// 场景1:直接传方法引用 → 调用时 this 是 undefined(丢失)
return <button onClick={this.handleClick}>按钮1</button>;
// 场景2:通过实例调用 → 调用时 this 是组件实例(正确)
// return <button onClick={() => this.handleClick()}>按钮2</button>;
}
}
关键:handleClick 里的 this,不是定义时确定的——写代码时你不知道它指向谁,只有点击按钮、方法被调用的那一刻,才知道 this 是啥。
- 按钮1:调用方式是「孤立调用」→
this= undefined; - 按钮2:调用方式是「实例.方法()」→
this= 组件实例。
2. 函数组件:静态词法作用域(定义时确定)
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log(count); // count 指向「定义时所在的作用域」
};
return <button onClick={handleClick}>按钮</button>;
}
关键:handleClick 里的 count,定义时就确定了——它属于 Counter 函数执行时的局部作用域,不管 handleClick 被传到哪、怎么调用,它内部访问的 count 永远是当前 Counter 作用域里的变量。
- 哪怕你把
handleClick传给子组件、甚至全局调用,它依然能拿到Counter里的count(因为词法作用域「锁死」了变量的查找路径); - 全程没有「动态绑定」,只有「静态的作用域查找」,所以永远不会找错变量。
再用「找路」的例子通俗解释
| 类组件(动态 this) | 函数组件(词法作用域) |
|---|---|
| 你告诉朋友:「到我家后,问主人在哪,然后跟主人走」→ 朋友到了之后,可能遇到假主人(this 指向错)、没主人(this=undefined),走丢; | 你告诉朋友:「沿着XX路直走,到3号楼2单元」→ 路线是固定的(定义时就确定),不管朋友什么时候来、怎么来,按路线走都能到,不会错; |
核心总结
- 类组件的坑:
this是动态绑定,指向由「调用方式」决定,写代码时无法确定,容易出错; - 函数组件的优势:利用 JS 的静态词法作用域,变量的查找路径在「定义时就固定」,不管怎么调用函数,都能精准找到对应的变量;
- 最终结果:函数组件从根源上避开了「动态绑定」的不确定性,不用再纠结「this 指向谁」,逻辑更稳定。
精准抓到「动态调用绑定 vs 静态词法作用域」这个核心,说明我们已经完全理解了 Hooks 函数组件避开 this 坑的底层逻辑!
类组件痛点2: 业务相关逻辑碎片化
一个业务逻辑(比如「请求数据并渲染」)往往需要拆分到多个生命周期里,导致相关逻辑被分散在不同函数中,阅读和维护成本极高。
class UserList extends React.Component {
state = { users: [], loading: true };
// 1. 组件挂载时请求数据 -->
componentDidMount() {
this.fetchUsers();
this.timer = setInterval(() => console.log('定时器'), 1000);
}
// 2. 组件更新时(比如 props 变化)重新请求数据 -->
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.fetchUsers();
}
}
// 3. 组件卸载时清理定时器 -->
componentWillUnmount() {
clearInterval(this.timer);
}
// 业务逻辑:请求用户数据
fetchUsers() {
fetch('/api/users')
.then(res => res.json())
.then(users => this.setState({ users, loading: false }));
}
render() { /* 渲染逻辑 */ }
}
你看:「请求数据 + 清理定时器」 这两个相关的逻辑,被拆到了 componentDidMount/componentDidUpdate/componentWillUnmount 三个生命周期里,代码跳来跳去,很难一眼看懂。
也就是说 业务逻辑强制和 react生命周期耦合到一起去了
类组件痛点3: 状态逻辑复用难、陷入嵌套地狱
先明确核心概念
- 状态逻辑复用:比如「跟踪鼠标位置」「表单校验」「登录状态管理」这些逻辑,多个组件都需要用,想抽出来复用;
- 嵌套地狱:为了复用逻辑,类组件只能用「高阶组件(HOC)」或「Render Props」,导致组件代码一层套一层,像剥洋葱一样难读。
第一步:看一个真实场景——复用「鼠标位置跟踪」逻辑
假设你有两个组件:MouseShow(显示鼠标位置)、MouseFollowBtn(按钮跟着鼠标动),都需要「跟踪鼠标位置」这个逻辑。
先写类组件的复用方案:Render Props(最典型的嵌套来源)
首先,把「鼠标跟踪」逻辑封装成一个通用组件(Render Props 模式):
// 通用的鼠标跟踪组件(Render Props 核心:把状态传给 children 函数)
class MouseTracker extends React.Component {
state = { x: 0, y: 0 };
// 监听鼠标移动,更新状态
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove);
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove);
}
handleMouseMove = (e) => {
this.setState({ x: e.clientX, y: e.clientY });
};
// 核心:把状态传给子组件(通过 children 函数)
render() {
return this.props.children(this.state);
}
}
然后,用这个组件实现「显示鼠标位置」:
// 第一个组件:显示鼠标位置
function MouseShow() {
return (
<div>
<h2>鼠标位置:</h2>
{/* 第一层嵌套:MouseTracker */}
<MouseTracker>
{/* children 是函数,接收鼠标状态 */}
{({ x, y }) => (
<p>X: {x}, Y: {y}</p>
)}
</MouseTracker>
</div>
);
}
再实现「按钮跟着鼠标动」:
// 第二个组件:按钮跟着鼠标动
function MouseFollowBtn() {
return (
<div>
{/* 第一层嵌套:MouseTracker */}
<MouseTracker>
{({ x, y }) => (
{/* 按钮样式绑定鼠标位置 */}
<button style={{ position: 'absolute', left: x, top: y }}>
跟着鼠标跑
</button>
)}
</MouseTracker>
</div>
);
}
问题来了:如果要复用多个逻辑,嵌套直接「地狱化」
现在需求升级:这两个组件不仅要「跟踪鼠标」,还要「复用主题样式」(比如深色/浅色模式)。
先封装「主题复用」的 Render Props 组件:
// 通用的主题组件
class ThemeProvider extends React.Component {
state = { theme: 'dark', color: 'white', bg: 'black' };
render() {
return this.props.children(this.state);
}
}
现在,MouseShow 要同时复用「鼠标+主题」逻辑,代码变成这样:
function MouseShow() {
return (
<div>
<h2>鼠标位置:</h2>
{/* 第一层嵌套:ThemeProvider */}
<ThemeProvider>
{/* 接收主题状态 */}
{({ theme, color, bg }) => (
{/* 第二层嵌套:MouseTracker */}
<MouseTracker>
{/* 接收鼠标状态 */}
{({ x, y }) => (
<p style={{ color, backgroundColor: bg }}>
【{theme}主题】X: {x}, Y: {y}
</p>
)}
</MouseTracker>
)}
</ThemeProvider>
</div>
);
}
如果再要加一个「用户登录状态」的复用逻辑,就会出现第三层嵌套:
<UserProvider>
{({ user }) => (
<ThemeProvider>
{({ theme, color, bg }) => (
<MouseTracker>
{({ x, y }) => (
<p>【{user.name}】【{theme}】X: {x}, Y: {y}</p>
)}
</MouseTracker>
)}
</ThemeProvider>
)}
</UserProvider>
这就是嵌套地狱:
- 代码层层缩进,一眼看不到头;
- 逻辑越复用,嵌套越深;
- 想改某个逻辑(比如换主题),要在嵌套里找半天,维护成本极高。
补充:高阶组件(HOC)的复用方式,同样逃不开嵌套
如果用 HOC 实现复用(比如 withMouse、withTheme),代码是这样的:
// 用 HOC 包装组件:一层套一层
const MouseShowWithTheme = withTheme(withMouse(MouseShow));
// 渲染时看似没有嵌套,但 HOC 本质是「组件套组件」,调试时 DevTools 里全是 HOC 包装层
// 比如 DevTools 里会显示:WithTheme(WithMouse(MouseShow))
调试时要一层层点开包装组件,才能找到真正的业务组件,同样痛苦。
函数组件说:没有hook,就别指望我了
千万别以为 函数组件是救星,React 早就有函数组件了,但是只是个纯展示的配角
先明确:Hooks 出现前,函数组件的「纯展示」本质
在 React 16.8(Hooks 诞生)之前,函数组件的官方定位就是 「无状态组件(Stateless Functional Component,SFC)」,核心特点:
- 没有自己的状态(
this.state); - 没有生命周期(
componentDidMount/render等); - 本质就是「输入 props → 输出 JSX」的纯函数——输入不变,输出就不变,没有任何副作用。
举个 Hooks 前的函数组件例子:
// 典型的「纯展示组件」:只接收 props,渲染 UI,无任何状态/副作用
function UserCard(props) {
const { name, avatar, age } = props;
return (
<div className="card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>年龄:{age}</p>
</div>
);
}
// 使用时:状态全靠父组件传递
class UserList extends React.Component {
state = {
users: [{ name: '张三', avatar: 'xxx.png', age: 20 }]
};
render() {
return (
<div>
{this.state.users.map(user => (
<UserCard key={user.name} {...user} />
))}
</div>
);
}
}
这个例子里:
-
UserCard是函数组件,只负责「展示」,没有任何自己的逻辑; - 所有状态(
users)、数据请求、生命周期逻辑,都必须写在父类组件UserList里; - 如果
UserCard想加个「点击头像放大」的交互(需要状态isZoom),对不起——函数组件做不到,必须把它改成类组件,或者把isZoom状态提到父组件里(增加父组件复杂度)。
为什么当时函数组件只能是「纯展示」?
核心原因是 React 的设计规则:
- 状态、生命周期、副作用这些「动态能力」,当时只开放给类组件;
- 函数组件被设计成「轻量、高效、无副作用」的最小渲染单元,目的是简化「纯展示场景」的代码(不用写
class/constructor等冗余代码)。
Hooks说:函数组件你别灰心,我让你从配角变主角
先拆清楚:函数组件 vs Hooks 的分工
1. 光有「函数组件」,解决不了任何问题
在 Hooks 出现之前,React 早就有函数组件了,但那时的函数组件是「纯展示组件」—— 没有状态、没有生命周期,只能接收 props 渲染 UI。
如果想在旧版函数组件中复用状态逻辑,依然只能用「Render Props/HOC」的嵌套方式:
// Hooks 出现前的函数组件:想复用逻辑,还是得嵌套
function MouseShow() {
return (
<MouseTracker>
{({x,y}) => <p>X:{x}, Y:{y}</p>}
</MouseTracker>
);
}
你看,哪怕是函数组件,没有 Hooks,依然逃不开嵌套 —— 因为没有「抽离状态逻辑」的工具。
2. Hooks 出现后:函数组件从「纯展示」→「全能选手」-- 痛点1迎刃而解
还是上面的 UserCard,Hooks 后可以直接加状态/副作用,不用依赖父组件:
import { useState } from 'react';
// 函数组件拥有了自己的状态,不再是「纯展示」
function UserCard(props) {
const { name, avatar, age } = props;
// 自己的状态:控制头像是否放大
const [isZoom, setIsZoom] = useState(false);
return (
<div className="card">
<img
src={avatar}
alt={name}
style={{ width: isZoom ? '200px' : '100px' }}
onClick={() => setIsZoom(!isZoom)} // 自己的交互逻辑
/>
<h3>{name}</h3>
<p>年龄:{age}</p>
</div>
);
}
此时函数组件的定位完全变了:
- 既能做「纯展示」(简单场景),也能做「有状态、有副作用、有复杂逻辑」的完整组件;
- 彻底替代了类组件的大部分场景,成为 React 官方推荐的写法。
是不是 彻底解决了 痛点1
3. 相关逻辑「聚在一起」,告别碎片化 -- 痛点2 再见
useEffect 一个 Hook 就能覆盖挂载、更新、卸载三个阶段的逻辑,让「请求数据 + 清理定时器」这样的相关逻辑写在同一个地方。
import { useState, useEffect } from 'react';
function UserList({ id }) {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
// 核心:请求数据 + 清理定时器 写在同一个 useEffect 里
useEffect(() => {
// 1. 挂载/更新时请求数据
const fetchUsers = async () => {
const res = await fetch(`/api/users?id=${id}`);
const data = await res.json();
setUsers(data);
setLoading(false);
};
fetchUsers();
// 2. 挂载时启动定时器
const timer = setInterval(() => console.log('定时器'), 1000);
// 3. 卸载/更新时清理副作用
return () => clearInterval(timer);
}, [id]); // 只有 id 变化时,才重新执行
return <div>{loading ? '加载中' : users.map(u => <div key={u.id}>{u.name}</div>)}</div>;
}
对比类组件的写法:所有相关逻辑都在一个 useEffect 里,不用在多个生命周期函数之间跳来跳去,可读性直接拉满。
4. Hooks 才是「解决嵌套问题的核心」
Hooks(尤其是自定义 Hooks)的核心价值,是「把状态逻辑从组件渲染流程中抽离出来,变成可调用的纯逻辑函数」。
Hooks 的核心思路是:把「状态逻辑」从「组件嵌套」中抽离出来,变成独立的「函数」,组件直接调用函数即可,没有任何嵌套。毕竟状态逻辑本来就跟 UI组件无关,为啥非要掺合在一起呢
还是上面的场景,用自定义 Hook 实现:
1. 抽离「鼠标跟踪」的自定义 Hook
import { useState, useEffect } from 'react';
// 自定义 Hook:抽离鼠标跟踪逻辑,返回鼠标位置
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return position;
}
2. 抽离「主题」的自定义 Hook
// 自定义 Hook:抽离主题逻辑,返回主题状态
function useTheme() {
const [theme, setTheme] = useState({ mode: 'dark', color: 'white', bg: 'black' });
return theme;
}
3. 组件中直接调用 Hook,没有任何嵌套
function MouseShow() {
// 调用自定义 Hook:平铺写法,没有嵌套
const { x, y } = useMousePosition(); // 复用鼠标逻辑
const { mode, color, bg } = useTheme(); // 复用主题逻辑
const { user } = useUser(); // 再复用用户逻辑,也只是多一行代码
return (
<p style={{ color, backgroundColor: bg }}>
【{user.name}】【{mode}主题】X: {x}, Y: {y}
</p>
);
}
-
useState/useEffect等内置 Hooks:让函数组件拥有了「状态」和「副作用处理能力」(这是抽离逻辑的基础); - 自定义 Hooks:把复用逻辑(比如鼠标跟踪、主题管理)封装成独立函数,让函数组件能「平铺调用」,而非「嵌套组件」。
对比类组件的嵌套地狱:
Hooks 是**平铺式复用**:不管复用多少逻辑,都是「调用函数 → 用状态」,代码没有任何缩进嵌套;逻辑和组件解耦:useMousePosition可以在任何组件里调用,不用套任何包装组件;-
调试简单:DevTools 里直接看到MouseShow组件,没有层层包装的 HOC/Render Props 组件。
核心总结(痛点+解决方案)
| 类组件复用逻辑(HOC/Render Props) | Hooks 复用逻辑(自定义 Hook) |
|---|---|
| 必须通过「组件嵌套」实现 | 直接调用「函数」,无嵌套 |
| 逻辑越多,嵌套越深(地狱化) | 逻辑越多,只是多几行函数调用 |
| 调试时要拆包装组件,成本高 | 调试直接看业务组件,逻辑清晰 |
简单来说,类组件的复用是「用组件套组件」,自然会嵌套;而 Hooks 的复用是「用函数抽逻辑」,组件只需要调用函数,从根源上消灭了嵌套地狱。 这也是 Hooks 最核心的价值之一——让状态逻辑复用变得简单、平铺、可维护。
最后总结
- 函数组件是「容器」:提供了「平铺写代码」的基础形式,但本身没有复用状态逻辑的能力;
-
Hooks 是「核心能力」:
- 内置 Hooks(
useState/useEffect)让函数组件能「持有状态、处理副作用」给函数组件「加手脚」; -
自定义 Hooks 让状态逻辑能「脱离组件嵌套,以函数形式被平铺调用」「状态与 UI 解耦」;
- 内置 Hooks(
- 最终结论:不是函数的功劳,也不是单纯 Hook 的功劳 —— 是「函数组件作为载体」+「Hooks 作为逻辑复用工具」的组合,才解决了一系列问题。
Hooks 是「矛」,函数组件是「握矛的手」—— 少了任何一个,都刺不穿嵌套地狱的盾。