
文章目录
-
- 一、痛点场景:为什么你写的React组件总是出问题?
- 二、痛点的解决方案:掌握组件API就是掌握React的命脉
- 三、是什么:React组件API到底是什么?
-
- [3.1 专业解释](#3.1 专业解释)
- [3.2 大白话解释](#3.2 大白话解释)
- [3.3 生活案例](#3.3 生活案例)
- 四、为什么用:组件API解决了什么问题?
-
- [4.1 问题一:如何让数据变化驱动界面自动更新?](#4.1 问题一:如何让数据变化驱动界面自动更新?)
- [4.2 问题二:如何高效地批量更新,避免性能浪费?](#4.2 问题二:如何高效地批量更新,避免性能浪费?)
- [4.3 问题三:如何在特殊情况下强制更新?](#4.3 问题三:如何在特殊情况下强制更新?)
- [4.4 问题四:如何规范组件的输入输出?](#4.4 问题四:如何规范组件的输入输出?)
- 五、它是怎么演进过来的:从createClass到Hooks的十年变迁
-
- [5.1 第一阶段:React.createClass 时代(2013 - 2016)](#5.1 第一阶段:React.createClass 时代(2013 - 2016))
- [5.2 第二阶段:ES6 Class 时代(2016 - 2019)](#5.2 第二阶段:ES6 Class 时代(2016 - 2019))
- [5.3 第三阶段:函数组件 + Hooks 时代(2019 - 至今)](#5.3 第三阶段:函数组件 + Hooks 时代(2019 - 至今))
- [5.4 演进对照表](#5.4 演进对照表)
- 六、怎么用:核心API实战详解
-
- [6.1 setState:最核心的状态更新API](#6.1 setState:最核心的状态更新API)
-
- [6.1.1 基本语法](#6.1.1 基本语法)
- [6.1.2 关键特性一:异步批处理](#6.1.2 关键特性一:异步批处理)
- [6.1.3 关键特性二:函数式更新解决连续调用问题](#6.1.3 关键特性二:函数式更新解决连续调用问题)
- [6.1.4 关键特性三:setState是同步还是异步?](#6.1.4 关键特性三:setState是同步还是异步?)
- [6.2 forceUpdate:强制重新渲染](#6.2 forceUpdate:强制重新渲染)
- [6.3 defaultProps:设置默认属性](#6.3 defaultProps:设置默认属性)
- [6.4 propTypes:属性类型校验](#6.4 propTypes:属性类型校验)
- [6.5 企业项目实战:封装一个可复用的计数器组件](#6.5 企业项目实战:封装一个可复用的计数器组件)
- 七、竞品对比:三种组件写法的优劣势分析
-
- [7.1 createClass的优势与劣势](#7.1 createClass的优势与劣势)
- [7.2 ES6 Class的优势与劣势](#7.2 ES6 Class的优势与劣势)
- [7.3 函数组件+Hooks的优势与劣势](#7.3 函数组件+Hooks的优势与劣势)
- 八、常用场景:企业项目中组件API的典型应用
-
- [8.1 场景一:表单数据管理](#8.1 场景一:表单数据管理)
- [8.2 场景二:列表数据加载与分页](#8.2 场景二:列表数据加载与分页)
- [8.3 场景三:props变化时同步更新state](#8.3 场景三:props变化时同步更新state)
- 九、面试官高频面试题
-
- 面试题一:setState是同步还是异步?
- 面试题二:为什么连续调用三次setState,结果只加了1?怎么解决?
- 面试题三:setState之后为什么读不到最新的state?怎么拿到最新值?
- 面试题四:forceUpdate的作用是什么?什么场景下使用?
- [面试题五:createClass、ES6 Class、函数组件三者的区别是什么?](#面试题五:createClass、ES6 Class、函数组件三者的区别是什么?)
- 面试题六:setState的第二个参数回调和componentDidUpdate有什么区别?
- 面试题七:React中state和props的区别是什么?
- 十、总结:从入门到精通的学习路径
一、痛点场景:为什么你写的React组件总是出问题?

你是否遇到过以下这些让人头秃的场景?
场景一:点击按钮计数,结果只加了1
你在一个事件处理函数里连续调用了三次 setState,满心期待计数器从0变成3,结果页面上只显示1。你反复检查代码逻辑,明明写了三次加1,为什么结果不对?
javascript
// 错误示范:连续调用三次setState,结果只加了1
handleClick = () => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出的还是旧值,比如0
};
场景二:setState之后立刻读取state,拿到的永远是旧值
你在调用 setState 之后马上 console.log(this.state),想看看更新后的数据,结果打印出来的还是更新前的值。你以为是React出了bug,其实是你不了解 setState 的异步批处理机制。
场景三:数据变了,界面就是不刷新
你直接修改了 this.state.xxx = newValue,数据确实变了,但页面纹丝不动。你不得不去搜"React修改数据不刷新",然后有人告诉你要用 setState,但你根本不知道为什么。
场景四:老项目里满屏的createClass,看不懂也不敢改
你接手了一个2016年的老项目,里面全是 React.createClass 的写法,getInitialState、getDefaultProps、mixins 这些API你见都没见过。想重构又怕搞崩,不重构又维护得痛苦不堪。
场景五:面试官问setState是同步还是异步,你支支吾吾答不上来
面试的时候,面试官微笑着问你:"React的setState是同步还是异步?"你脑子里一片混乱,好像在哪里看过说异步,又好像在某些情况下是同步,结果答得前后矛盾,面试直接凉凉。
如果你中了以上任意一条,那么这篇文章就是为你写的。接下来,我们将从根源上彻底搞懂React组件API,让你不再被这些问题困扰。
二、痛点的解决方案:掌握组件API就是掌握React的命脉
以上所有痛点的根源,都在于对React组件API的理解不够深入。React组件API是React的核心基石,它决定了:
- 组件如何管理自身状态(state)
- 组件如何接收外部数据(props)
- 组件如何触发界面更新(setState / forceUpdate)
- 组件如何在不同阶段执行逻辑(生命周期方法)
只要彻底掌握了组件API,上面的所有问题都将迎刃而解:
- 连续调用setState只加1?用函数式更新
this.setState(prev => ({ count: prev.count + 1 }))即可解决。 - setState后读不到最新值?用第二个参数回调
this.setState(updater, () => console.log(this.state))。 - 直接修改state不刷新?永远通过
setState更新状态,保持数据不可变性。 - 老项目createClass看不懂?了解演进历史,掌握createClass到ES6 Class再到Hooks的对应关系。
- 面试答不上setState同步异步?理解批处理机制,分场景回答即可。
下面,我们正式进入React组件API的世界。
三、是什么:React组件API到底是什么?

3.1 专业解释
React组件API(Component API)是React框架为组件实例提供的一组内置方法和属性的集合。它是React组件与框架核心之间的契约接口,开发者通过这些API来管理组件状态、触发重新渲染、访问组件实例属性,从而实现动态的用户界面。
在类组件时代,组件API主要包括以下核心成员:
| API类别 | 具体方法/属性 | 作用 |
|---|---|---|
| 状态更新 | setState(updater, callback?) |
将状态变更加入队列,触发重新渲染 |
| 强制更新 | forceUpdate(callback?) |
跳过shouldComponentUpdate,强制重新渲染 |
| 实例属性 | this.props |
组件接收的外部属性(只读) |
| 实例属性 | this.state |
组件自身的状态数据 |
| 实例属性 | this.context |
组件的上下文数据 |
| 实例属性 | this.refs |
组件的DOM引用(已废弃) |
| 类属性 | defaultProps |
设置props的默认值 |
| 类属性 | displayName |
组件显示名称,用于调试 |
| 类属性 | propTypes |
props类型校验(已移至独立包) |
3.2 大白话解释
说人话,React组件API就像是你买的一台智能洗衣机的控制面板。
setState就是那个"启动"按钮,你按一下,洗衣机就开始工作(重新渲染)。但你连续快速按三下,洗衣机不会启动三次,它只会把你的指令合并成一次来执行------这就是批处理。forceUpdate就是那个"强制启动"按钮,不管洗衣机现在是什么状态,按了就必须转。但平时你不应该用它,因为正常流程已经够用了。this.props就是你放进去的衣服,是外部给的,你不能在洗衣机里改衣服本身。this.state就是洗衣机当前的运行模式(洗涤、漂洗、脱水),是洗衣机自己管理的,你可以通过按钮来切换。render()就是洗衣机真正开始转的那个过程,把衣服洗干净(把数据变成界面)。
3.3 生活案例
想象你在一家餐厅点餐:
- 你(父组件)把菜单(props)递给服务员(子组件),服务员不能修改菜单上的价格,这就是props的只读性。
- 服务员(子组件)自己有一个小本本(state),记录着当前桌号、已点菜品、是否上菜等信息,这些信息服务员可以自己更新。
- 每当服务员在小本本上记了新东西(调用setState),厨房(React渲染引擎)就会收到通知,开始准备菜品(重新渲染)。
- 如果服务员一次性点了三个菜(连续三次setState),厨房不会分三次做,而是把三个菜合并成一单一起做------这就是批处理,效率更高。
forceUpdate就像是服务员直接冲进厨房说"别管流程了,马上给我做",虽然能用,但会打乱厨房的正常节奏,所以不推荐。
四、为什么用:组件API解决了什么问题?
4.1 问题一:如何让数据变化驱动界面自动更新?
在原生JavaScript时代,更新界面需要手动操作DOM:
javascript
// 原生JS更新界面:手动操作DOM
const countElement = document.getElementById('count');
let count = 0;
document.getElementById('btn').addEventListener('click', () => {
count += 1;
countElement.textContent = count; // 手动更新DOM
});
这种方式在复杂应用中会变成灾难------你需要在无数个地方手动同步数据和DOM,稍有遗漏就会出现数据和界面不一致的bug。
React组件API通过 setState 解决了这个问题:你只需要关心数据怎么变,React会自动帮你更新界面。
javascript
// React更新界面:只改数据,界面自动更新
class Counter extends React.Component {
state = { count: 0 };
handleClick = () => {
this.setState({ count: this.state.count + 1 }); // 只改数据
};
render() {
return <div>{this.state.count}</div>; // 界面自动同步
}
}
4.2 问题二:如何高效地批量更新,避免性能浪费?
如果每次 setState 都立即重新渲染,那么在一个事件处理函数中调用10次 setState 就会触发10次重新渲染,这对性能来说是巨大的浪费。
React组件API通过**批处理(Batching)**机制解决了这个问题:在同一个事件处理函数中,所有的 setState 会被合并,最终只触发一次重新渲染。
4.3 问题三:如何在特殊情况下强制更新?
虽然绝大多数情况下通过 setState 更新状态就够了,但在某些极端场景下(比如render依赖了外部可变数据、集成第三方库等),你可能需要绕过正常的更新流程强制重新渲染。forceUpdate 就是为这种场景准备的逃生舱。
4.4 问题四:如何规范组件的输入输出?
defaultProps 和 propTypes 让组件的接口更加清晰:父组件传什么、默认值是什么、类型对不对,一目了然。这在团队协作和大型项目中尤为重要,能在开发阶段就发现很多潜在的bug。
五、它是怎么演进过来的:从createClass到Hooks的十年变迁

React组件API的演进史,就是一部前端开发范式的变革史。理解这段历史,你才能真正看懂为什么今天的React是这个样子。
5.1 第一阶段:React.createClass 时代(2013 - 2016)
React在2013年开源时,ES6还没有被广泛支持,所以React提供了 React.createClass 这个工厂函数来创建组件。
javascript
// React.createClass 写法(已废弃)
var Counter = React.createClass({
// 初始化state
getInitialState: function () {
return { count: 0 };
},
// 设置默认props
getDefaultProps: function () {
return { step: 1 };
},
// props类型校验
propTypes: {
step: React.PropTypes.number
},
// 自动绑定this
handleClick: function () {
this.setState({ count: this.state.count + this.props.step });
},
render: function () {
return (
<div onClick={this.handleClick}>
{this.state.count}
</div>
);
}
});
特点:
- 不需要写
class关键字,用普通对象描述组件 getInitialState初始化状态,getDefaultProps设置默认属性- 方法自动绑定
this,不需要手动bind - 支持
mixins混入机制实现逻辑复用 - 代码冗长,不符合ES6规范
5.2 第二阶段:ES6 Class 时代(2016 - 2019)
随着ES6的普及,React在0.13版本引入了ES6 Class语法,createClass 逐渐被弃用(在React 15.5.0中被标记为废弃,移至独立的 create-react-class 包)。
javascript
// ES6 Class 写法
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Counter extends Component {
// 初始化state(类属性语法)
state = { count: 0 };
// 默认props
static defaultProps = { step: 1 };
// props类型校验
static propTypes = {
step: PropTypes.number
};
// 需要手动绑定this,或者用箭头函数类属性
handleClick = () => {
this.setState({ count: this.state.count + this.props.step });
};
render() {
return (
<div onClick={this.handleClick}>
{this.state.count}
</div>
);
}
}
特点:
- 使用标准ES6
class语法 constructor中初始化state,或使用类属性state = {}static defaultProps设置默认属性propTypes移至独立的prop-types包- 方法不会自动绑定
this,需要手动bind或使用箭头函数 mixins被废弃,改用高阶组件(HOC)和render props实现逻辑复用
5.3 第三阶段:函数组件 + Hooks 时代(2019 - 至今)
2019年2月,React 16.8正式发布Hooks。Hooks让函数组件也能拥有state和生命周期能力,彻底改变了React的开发范式。
javascript
// 函数组件 + Hooks 写法
import React, { useState } from 'react';
import PropTypes from 'prop-types';
function Counter({ step = 1 }) {
// useState管理状态
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + step);
};
return (
<div onClick={handleClick}>
{count}
</div>
);
}
Counter.propTypes = {
step: PropTypes.number
};
特点:
- 不需要
class,纯函数写法 useState管理状态,useEffect处理副作用- 没有
this绑定问题 - 自定义Hook实现逻辑复用,比HOC更优雅
- 代码更简洁,逻辑更内聚
- React官方推荐的写法
5.4 演进对照表
| 特性 | createClass | ES6 Class | 函数组件+Hooks |
|---|---|---|---|
| 创建方式 | React.createClass({}) |
class X extends Component |
function X() {} |
| 状态初始化 | getInitialState() |
constructor 或 state = {} |
useState(initial) |
| 默认props | getDefaultProps() |
static defaultProps |
函数参数默认值 |
| 类型校验 | propTypes 对象属性 |
static propTypes |
函数属性 propTypes |
| this绑定 | 自动绑定 | 需手动bind或箭头函数 | 无this |
| 逻辑复用 | mixins | HOC / render props | 自定义Hook |
| 生命周期 | 生命周期方法 | 生命周期方法 | useEffect等Hook |
| 代码量 | 多 | 中 | 少 |
| 当前状态 | 已废弃 | 仍支持但不推荐 | 官方推荐 |
六、怎么用:核心API实战详解

6.1 setState:最核心的状态更新API
setState 是React组件API中最重要、最常用的方法,没有之一。
6.1.1 基本语法
javascript
// 形式一:传入对象(简单场景)
this.setState({
key: newValue
});
// 形式二:传入函数(推荐,基于前一个状态计算)
this.setState((prevState, props) => {
return {
key: prevState.key + props.step
};
});
// 形式三:带回调函数(更新完成后执行)
this.setState(
{ key: newValue },
() => {
console.log('更新完成,最新state:', this.state);
}
);
6.1.2 关键特性一:异步批处理
setState 不会立即更新 this.state,而是将更新加入队列,等当前事件处理函数执行完毕后再批量处理。
javascript
class Counter extends React.Component {
state = { count: 0 };
handleClick = () => {
// 三次调用都会被合并,最终只渲染一次
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 这里读到的还是旧值 0
console.log('立即读取:', this.state.count);
};
render() {
console.log('render执行, count:', this.state.count);
return (
<div>
<p>计数: {this.state.count}</p>
<button onClick={this.handleClick}>点我</button>
</div>
);
}
}
运行结果:
- 点击一次按钮,
render只执行一次 count从0变成1(不是3),因为三次setState都基于同一个旧值0计算,合并后结果是1立即读取: 0
6.1.3 关键特性二:函数式更新解决连续调用问题
如果需要基于前一个状态连续更新,必须使用函数式更新:
javascript
handleClick = () => {
// 使用函数式更新,每次都基于最新的pending state计算
this.setState(prev => ({ count: prev.count + 1 }));
this.setState(prev => ({ count: prev.count + 1 }));
this.setState(prev => ({ count: prev.count + 1 }));
};
运行结果: 点击一次,count 从0变成3。因为函数式更新会被放入队列依次执行,每次都能拿到上一次更新后的结果。
6.1.4 关键特性三:setState是同步还是异步?
这是面试高频题,答案是:在React合成事件和生命周期中是异步(批处理)的,在原生DOM事件、setTimeout、Promise等异步回调中是同步的(React 18之前)。React 18之后默认全部自动批处理。
javascript
class Demo extends React.Component {
state = { count: 0 };
// React合成事件中:异步批处理
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log('合成事件中:', this.state.count); // 0(旧值)
};
componentDidMount() {
// setTimeout中:React 17及之前是同步,React 18是异步批处理
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log('setTimeout中:', this.state.count);
// React 17: 1(新值,同步)
// React 18: 0(旧值,自动批处理)
}, 0);
// 原生DOM事件中:React 17及之前是同步
document.getElementById('btn').addEventListener('click', () => {
this.setState({ count: this.state.count + 1 });
console.log('原生事件中:', this.state.count);
// React 17: 新值(同步)
// React 18: 旧值(自动批处理)
});
}
}
6.2 forceUpdate:强制重新渲染
forceUpdate 会跳过 shouldComponentUpdate,强制组件重新渲染。
javascript
class ForceDemo extends React.Component {
// 故意返回false,阻止正常更新
shouldComponentUpdate() {
return false;
}
handleForceUpdate = () => {
// 强制更新,跳过shouldComponentUpdate
this.forceUpdate(() => {
console.log('强制更新完成');
});
};
render() {
return (
<div>
<p>当前时间: {new Date().toLocaleTimeString()}</p>
<button onClick={this.handleForceUpdate}>强制刷新</button>
</div>
);
}
}
使用场景(极少用到):
- render方法依赖了外部可变数据(不推荐,应该用state管理)
- 集成某些第三方库需要手动触发更新
- 临时调试用
注意: forceUpdate 只会触发当前组件的重新渲染,子组件仍然会正常执行 shouldComponentUpdate。官方强烈不推荐使用 forceUpdate,绝大多数场景都应该通过 setState 来驱动更新。
6.3 defaultProps:设置默认属性
javascript
// ES6 Class 写法
class Greeting extends React.Component {
static defaultProps = {
name: '陌生人',
age: 18
};
render() {
return <div>你好,{this.props.name},今年{this.props.age}岁</div>;
}
}
// 使用:不传name和age时,使用默认值
<Greeting /> // 你好,陌生人,今年18岁
<Greeting name="张三" age={25} /> // 你好,张三,今年25岁
6.4 propTypes:属性类型校验
javascript
import PropTypes from 'prop-types';
class UserCard extends React.Component {
static propTypes = {
// 基础类型
name: PropTypes.string.isRequired, // 必填字符串
age: PropTypes.number, // 可选数字
isVip: PropTypes.bool, // 可选布尔值
// 数组和对象
hobbies: PropTypes.arrayOf(PropTypes.string), // 字符串数组
address: PropTypes.shape({
city: PropTypes.string,
street: PropTypes.string
}), // 指定结构的对象
// 函数
onClick: PropTypes.func,
// 枚举
status: PropTypes.oneOf(['active', 'inactive', 'pending']),
// 多种类型
data: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
};
static defaultProps = {
age: 18,
isVip: false
};
render() {
return <div>{/* ... */}</div>;
}
}
6.5 企业项目实战:封装一个可复用的计数器组件
下面是一个在企业项目中常见的、符合最佳实践的计数器组件:
javascript
// src/components/Counter/index.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './Counter.css';
class Counter extends Component {
static propTypes = {
initialValue: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
onChange: PropTypes.func
};
static defaultProps = {
initialValue: 0,
min: -Infinity,
max: Infinity,
step: 1,
onChange: () => {}
};
state = {
count: this.props.initialValue
};
// 函数式更新,确保基于最新状态计算
handleIncrement = () => {
this.setState(
prevState => {
const nextCount = Math.min(prevState.count + this.props.step, this.props.max);
return { count: nextCount };
},
() => {
this.props.onChange(this.state.count);
}
);
};
handleDecrement = () => {
this.setState(
prevState => {
const nextCount = Math.max(prevState.count - this.props.step, this.props.min);
return { count: nextCount };
},
() => {
this.props.onChange(this.state.count);
}
);
};
handleReset = () => {
this.setState(
{ count: this.props.initialValue },
() => {
this.props.onChange(this.state.count);
}
);
};
render() {
const { count } = this.state;
const { min, max } = this.props;
return (
<div className="counter-container">
<button
className="counter-btn"
onClick={this.handleDecrement}
disabled={count <= min}
>
-
</button>
<span className="counter-value">{count}</span>
<button
className="counter-btn"
onClick={this.handleIncrement}
disabled={count >= max}
>
+
</button>
<button className="counter-reset" onClick={this.handleReset}>
重置
</button>
</div>
);
}
}
export default Counter;
css
/* src/components/Counter/Counter.css */
.counter-container {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #f5f5f5;
border-radius: 8px;
}
.counter-btn {
width: 36px;
height: 36px;
border: none;
border-radius: 4px;
background: #1890ff;
color: #fff;
font-size: 18px;
cursor: pointer;
}
.counter-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.counter-value {
min-width: 60px;
text-align: center;
font-size: 20px;
font-weight: bold;
}
.counter-reset {
margin-left: auto;
padding: 6px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #fff;
cursor: pointer;
}
javascript
// 使用示例
import React from 'react';
import Counter from './components/Counter';
function App() {
const handleChange = value => {
console.log('计数器变化:', value);
};
return (
<div style={{ padding: 24 }}>
<h2>商品数量选择</h2>
<Counter
initialValue={1}
min={1}
max={99}
step={1}
onChange={handleChange}
/>
</div>
);
}
export default App;
七、竞品对比:三种组件写法的优劣势分析

| 对比维度 | React.createClass | ES6 Class Component | 函数组件 + Hooks |
|---|---|---|---|
| 语法规范 | 非标准,React自定义工厂函数 | 标准ES6 Class语法 | 标准JavaScript函数 |
| 状态管理 | getInitialState() 返回对象 |
this.state + setState |
useState / useReducer |
| 生命周期 | 完整生命周期方法 | 完整生命周期方法 | useEffect / useLayoutEffect 等 |
| this绑定 | 自动绑定,无需处理 | 需手动bind或箭头函数类属性 | 无this概念 |
| 逻辑复用 | mixins(已废弃,有命名冲突) | HOC / render props(嵌套地狱) | 自定义Hook(最优雅) |
| 代码量 | 多,样板代码多 | 中等,需要class和constructor | 少,简洁直观 |
| 学习曲线 | 低,概念简单 | 中,需要理解class和this | 中高,需要理解闭包和Hook规则 |
| 性能优化 | PureRenderMixin |
PureComponent / shouldComponentUpdate |
React.memo / useMemo / useCallback |
| 类型推导 | 差 | 一般(需手动声明) | 好(TypeScript天然支持) |
| 测试友好度 | 一般 | 一般(需mock实例) | 好(纯函数,易测试) |
| 当前状态 | 已废弃(React 15.5+) | 仍支持但不推荐新代码使用 | 官方推荐,未来方向 |
| 适用场景 | 维护2016年之前的老项目 | 维护存量类组件项目 | 所有新项目首选 |
7.1 createClass的优势与劣势
优势:
- 对初学者友好,不需要理解class和this
- 自动绑定this,不会出现
undefined错误 - mixins在当时是一种不错的逻辑复用方案
劣势:
- 非标准语法,与JavaScript生态脱节
- mixins存在命名冲突、隐式依赖、难以维护等问题
- 代码冗长,大量样板代码
- 已被官方废弃,新项目不应使用
7.2 ES6 Class的优势与劣势
优势:
- 标准ES6语法,符合JavaScript规范
- 生命周期方法清晰,适合理解React的执行流程
- 面向对象的写法,对有后端经验的开发者友好
PureComponent提供了浅比较优化
劣势:
- this绑定问题困扰初学者
- 逻辑分散在多个生命周期方法中,相关逻辑不内聚
- HOC和render props导致嵌套地狱
- 代码量仍然较多
- React官方已不推荐在新代码中使用
7.3 函数组件+Hooks的优势与劣势
优势:
- 代码简洁,没有class、constructor、this绑定
- 相关逻辑可以内聚在一个useEffect中,不再分散
- 自定义Hook实现逻辑复用,比HOC优雅得多
- 更好的TypeScript类型推导
- 更容易测试(纯函数)
- React官方推荐,持续获得新特性支持
劣势:
- 学习曲线较陡,需要理解闭包、Hook规则、依赖数组等概念
useEffect的依赖数组容易出错- 某些高级特性(如getSnapshotBeforeUpdate、Error Boundaries)仍需class组件
- 老项目迁移需要成本
八、常用场景:企业项目中组件API的典型应用
8.1 场景一:表单数据管理
javascript
class LoginForm extends React.Component {
state = {
username: '',
password: '',
loading: false,
error: ''
};
// 通用的input change处理,用属性名动态更新
handleInputChange = field => e => {
this.setState({
[field]: e.target.value,
error: ''
});
};
handleSubmit = async e => {
e.preventDefault();
const { username, password } = this.state;
if (!username || !password) {
this.setState({ error: '用户名和密码不能为空' });
return;
}
this.setState({ loading: true, error: '' });
try {
// 模拟API请求
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('登录成功:', { username, password });
this.setState({ loading: false });
} catch (err) {
this.setState({ loading: false, error: '登录失败,请重试' });
}
};
render() {
const { username, password, loading, error } = this.state;
return (
<form onSubmit={this.handleSubmit}>
{error && <div className="error-message">{error}</div>}
<input
type="text"
value={username}
onChange={this.handleInputChange('username')}
placeholder="用户名"
disabled={loading}
/>
<input
type="password"
value={password}
onChange={this.handleInputChange('password')}
placeholder="密码"
disabled={loading}
/>
<button type="submit" disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
</form>
);
}
}
8.2 场景二:列表数据加载与分页
javascript
class UserList extends React.Component {
state = {
users: [],
page: 1,
pageSize: 10,
total: 0,
loading: false
};
componentDidMount() {
this.fetchUsers();
}
fetchUsers = async () => {
const { page, pageSize } = this.state;
this.setState({ loading: true });
try {
const res = await fetch(
`/api/users?page=${page}&pageSize=${pageSize}`
);
const data = await res.json();
this.setState({
users: data.list,
total: data.total,
loading: false
});
} catch (err) {
this.setState({ loading: false });
console.error('加载用户列表失败:', err);
}
};
handlePageChange = page => {
// 使用函数式更新,确保page是最新的
this.setState({ page }, () => {
this.fetchUsers(); // 在回调中确保page已更新
});
};
render() {
const { users, loading, page, pageSize, total } = this.state;
return (
<div>
{loading ? (
<div>加载中...</div>
) : (
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
</tr>
))}
</tbody>
</table>
)}
<div className="pagination">
<span>共 {total} 条</span>
<button
onClick={() => this.handlePageChange(page - 1)}
disabled={page <= 1}
>
上一页
</button>
<span>第 {page} 页</span>
<button
onClick={() => this.handlePageChange(page + 1)}
disabled={page * pageSize >= total}
>
下一页
</button>
</div>
</div>
);
}
}
8.3 场景三:props变化时同步更新state
javascript
class PriceDisplay extends React.Component {
state = {
displayPrice: this.props.price
};
// 当父组件传入的price变化时,同步更新内部state
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.price !== prevState.lastPrice) {
return {
displayPrice: nextProps.price,
lastPrice: nextProps.price
};
}
return null;
}
handleDiscount = () => {
this.setState(prev => ({
displayPrice: (prev.displayPrice * 0.8).toFixed(2)
}));
};
render() {
return (
<div>
<p>价格: ¥{this.state.displayPrice}</p>
<button onClick={this.handleDiscount}>打八折</button>
</div>
);
}
}
九、面试官高频面试题

面试题一:setState是同步还是异步?
参考答案:
这个问题需要分版本和场景来回答:
-
React 17及之前:
- 在React合成事件(如onClick)和生命周期方法中,setState是异步的(批处理),多次调用会合并为一次更新。
- 在原生DOM事件(addEventListener)、setTimeout、setInterval、Promise.then等异步回调中,setState是同步的,调用后立即更新。
-
React 18及之后:
- 引入了自动批处理(Automatic Batching),所有场景下默认都是异步批处理的,包括setTimeout、Promise等异步回调。
- 如果需要同步更新,可以使用
flushSync。
-
本质原因:
- setState本身的执行是同步的,所谓的"异步"是指React的批处理机制------将多次状态更新合并,在事件处理函数结束后统一执行,以提升性能。
javascript
import { flushSync } from 'react-dom';
// React 18中强制同步更新
handleClick = () => {
flushSync(() => {
this.setState({ count: this.state.count + 1 });
});
console.log(this.state.count); // 立即拿到最新值
};
面试题二:为什么连续调用三次setState,结果只加了1?怎么解决?
参考答案:
原因:对象式的setState会被React批处理合并,三次调用都基于同一个旧的state值计算,最终结果只相当于加了一次。
javascript
// 问题代码
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 三次都基于 this.state.count = 0 计算,合并后 count = 1
解决方案:使用函数式更新(updater function),每次都基于队列中上一次更新后的pending state计算。
javascript
// 正确代码
this.setState(prev => ({ count: prev.count + 1 }));
this.setState(prev => ({ count: prev.count + 1 }));
this.setState(prev => ({ count: prev.count + 1 }));
// 依次执行:0 -> 1 -> 2 -> 3,最终 count = 3
面试题三:setState之后为什么读不到最新的state?怎么拿到最新值?
参考答案:
原因:setState是异步批处理的,调用后 this.state 不会立即更新,所以紧接着读取拿到的是旧值。
拿到最新值的三种方式:
- 使用setState的第二个参数回调:
javascript
this.setState(
{ count: this.state.count + 1 },
() => {
console.log('最新值:', this.state.count); // 更新完成后执行,拿到最新值
}
);
- 在componentDidUpdate中读取:
javascript
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
console.log('最新值:', this.state.count);
}
}
- 使用函数式更新直接计算(不需要读取state):
javascript
this.setState(prev => {
const newCount = prev.count + 1;
console.log('计算中的最新值:', newCount);
return { count: newCount };
});
面试题四:forceUpdate的作用是什么?什么场景下使用?
参考答案:
forceUpdate 会强制组件重新渲染,跳过 shouldComponentUpdate 的判断。
使用场景(极少用到):
- render方法依赖了外部可变数据(不推荐,应该将数据纳入state管理)
- 集成第三方库时需要手动触发更新
- 临时调试使用
注意事项:
forceUpdate只会跳过当前组件的shouldComponentUpdate,子组件仍然会正常执行生命周期方法- 官方强烈不推荐使用,绝大多数场景应该通过
setState驱动更新 - 调用
forceUpdate会触发componentWillUpdate和componentDidUpdate
面试题五:createClass、ES6 Class、函数组件三者的区别是什么?
参考答案:
这是React组件写法的三个演进阶段:
-
createClass(React 0.3 - 15.4):
- 使用
React.createClass({})工厂函数创建 getInitialState()初始化状态,getDefaultProps()设置默认属性- 方法自动绑定this
- 使用mixins实现逻辑复用(已废弃)
- 已被官方废弃
- 使用
-
ES6 Class(React 0.13 - 至今):
- 使用
class X extends React.Component创建 - constructor或类属性初始化state,
static defaultProps设置默认属性 - 需要手动绑定this(bind或箭头函数)
- 使用HOC或render props实现逻辑复用
- 仍支持但不推荐新代码使用
- 使用
-
函数组件+Hooks(React 16.8 - 至今):
- 使用普通函数创建组件
useState管理状态,useEffect处理副作用- 没有this绑定问题
- 自定义Hook实现逻辑复用(最优雅)
- 代码简洁,官方推荐
面试题六:setState的第二个参数回调和componentDidUpdate有什么区别?
参考答案:
| 对比项 | setState回调 | componentDidUpdate |
|---|---|---|
| 触发时机 | 本次setState更新完成后立即执行 | 组件每次更新完成后执行 |
| 执行范围 | 只针对调用setState的那次更新 | 所有原因导致的更新都会触发 |
| 参数 | 无参数,直接访问this.state | (prevProps, prevState, snapshot) |
| 使用场景 | 需要在特定setState后立即执行某些操作 | 需要监听所有状态变化并做相应处理 |
| 性能 | 只在需要时执行,更精准 | 每次更新都执行,可能有额外开销 |
javascript
// setState回调:只在这次更新后执行
this.setState({ data: newData }, () => {
console.log('数据更新完成:', this.state.data);
});
// componentDidUpdate:每次更新都会执行
componentDidUpdate(prevProps, prevState) {
if (prevState.data !== this.state.data) {
console.log('data发生了变化');
}
}
面试题七:React中state和props的区别是什么?
参考答案:
| 对比项 | state | props |
|---|---|---|
| 来源 | 组件自身管理 | 父组件传入 |
| 可修改性 | 组件内部可通过setState修改 | 只读,组件不能修改自己的props |
| 作用 | 管理组件内部状态 | 组件间通信,父传子数据 |
| 触发渲染 | setState会触发重新渲染 | props变化会触发重新渲染 |
| 默认值 | getInitialState / state = {} | getDefaultProps / defaultProps |
| 类比 | 组件的"私有变量" | 组件的"函数参数" |
一句话总结:props是外部传入的只读数据,state是组件内部管理的可变数据,两者变化都会触发组件重新渲染。
十、总结:从入门到精通的学习路径
通过本文的学习,你应该已经掌握了React组件API的核心知识。回顾一下我们的学习路径:
- 痛点场景:识别了日常开发中常见的5大坑
- 解决方案:明确了掌握组件API是解决所有问题的关键
- 是什么:理解了组件API的定义和核心成员
- 为什么用:明白了组件API解决的四大问题
- 演进历史:了解了从createClass到Hooks的十年变迁
- 怎么用:掌握了setState、forceUpdate、defaultProps、propTypes的实战用法
- 竞品对比:对比了三种组件写法的优劣势
- 常用场景:学习了表单、列表、props同步等企业级实战
- 面试题:准备了7道高频面试题的标准答案
React组件API是React的基石,虽然Hooks已经成为主流,但理解类组件时代的API仍然非常重要------因为大量存量项目还在使用类组件,而且Hooks的很多设计思想都源于类组件API。掌握了这些,你无论是维护老项目还是开发新项目,都能游刃有余。
本文为原创文章,如需转载,请联系作者获得授权,并注明出处。