08_React组件API

文章目录

一、痛点场景:为什么你写的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是同步还是异步?

参考答案:

这个问题需要分版本和场景来回答:

  1. React 17及之前:

    • 在React合成事件(如onClick)和生命周期方法中,setState是异步的(批处理),多次调用会合并为一次更新。
    • 在原生DOM事件(addEventListener)、setTimeout、setInterval、Promise.then等异步回调中,setState是同步的,调用后立即更新。
  2. React 18及之后:

    • 引入了自动批处理(Automatic Batching),所有场景下默认都是异步批处理的,包括setTimeout、Promise等异步回调。
    • 如果需要同步更新,可以使用 flushSync。
  3. 本质原因:

    • 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 不会立即更新,所以紧接着读取拿到的是旧值。

拿到最新值的三种方式:

  1. 使用setState的第二个参数回调:
javascript 复制代码
this.setState(
  { count: this.state.count + 1 },
  () => {
    console.log('最新值:', this.state.count); // 更新完成后执行,拿到最新值
  }
);
  1. 在componentDidUpdate中读取:
javascript 复制代码
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    console.log('最新值:', this.state.count);
  }
}
  1. 使用函数式更新直接计算(不需要读取state):
javascript 复制代码
this.setState(prev => {
  const newCount = prev.count + 1;
  console.log('计算中的最新值:', newCount);
  return { count: newCount };
});

面试题四:forceUpdate的作用是什么?什么场景下使用?

参考答案:

forceUpdate 会强制组件重新渲染,跳过 shouldComponentUpdate 的判断。

使用场景(极少用到):

  1. render方法依赖了外部可变数据(不推荐,应该将数据纳入state管理)
  2. 集成第三方库时需要手动触发更新
  3. 临时调试使用

注意事项:

  • forceUpdate 只会跳过当前组件的 shouldComponentUpdate,子组件仍然会正常执行生命周期方法
  • 官方强烈不推荐使用,绝大多数场景应该通过 setState 驱动更新
  • 调用 forceUpdate 会触发 componentWillUpdate 和 componentDidUpdate

面试题五:createClass、ES6 Class、函数组件三者的区别是什么?

参考答案:

这是React组件写法的三个演进阶段:

  1. createClass(React 0.3 - 15.4):

    • 使用 React.createClass({}) 工厂函数创建
    • getInitialState() 初始化状态,getDefaultProps() 设置默认属性
    • 方法自动绑定this
    • 使用mixins实现逻辑复用(已废弃)
    • 已被官方废弃
  2. ES6 Class(React 0.13 - 至今):

    • 使用 class X extends React.Component 创建
    • constructor或类属性初始化state,static defaultProps 设置默认属性
    • 需要手动绑定this(bind或箭头函数)
    • 使用HOC或render props实现逻辑复用
    • 仍支持但不推荐新代码使用
  3. 函数组件+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的核心知识。回顾一下我们的学习路径:

  1. 痛点场景:识别了日常开发中常见的5大坑
  2. 解决方案:明确了掌握组件API是解决所有问题的关键
  3. 是什么:理解了组件API的定义和核心成员
  4. 为什么用:明白了组件API解决的四大问题
  5. 演进历史:了解了从createClass到Hooks的十年变迁
  6. 怎么用:掌握了setState、forceUpdate、defaultProps、propTypes的实战用法
  7. 竞品对比:对比了三种组件写法的优劣势
  8. 常用场景:学习了表单、列表、props同步等企业级实战
  9. 面试题:准备了7道高频面试题的标准答案

React组件API是React的基石,虽然Hooks已经成为主流,但理解类组件时代的API仍然非常重要------因为大量存量项目还在使用类组件,而且Hooks的很多设计思想都源于类组件API。掌握了这些,你无论是维护老项目还是开发新项目,都能游刃有余。


本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
小马不起床1 小时前
零基础玩转 FastAPI_09:前端基础工程构建
前端·状态模式·fastapi
风骏时光牛马2 小时前
用户管理模块接口开发
前端
彭亚川Allen3 小时前
从零手搓 RAG 之检索全流程,给大模型接上自己的知识库
前端·后端·ai编程
IT_陈寒3 小时前
别让Java字符串拼接偷偷吃掉你的内存
前端·人工智能·后端
软件技术NINI3 小时前
圣罗兰彩妆html+css 4页
前端·css·html
2601_951615204 小时前
网站模板 网站网页模板下载
前端·网络·html
酷炫码神4 小时前
Day 1|HTML5:一个商城首页的骨架,从 DOCTYPE 到语义化
前端·html·html5
AI源码5 小时前
鲜花商城(HTML 版)
前端·html
hiahiahia1235 小时前
Message 和对话
服务器·前端·数据库