Skip to content

React 文档

导读与全书目录表:见同目录 01-React 入门:简介·特性与设计范式.md 开头。


在 react 中定义和修改 state

在函数组件中

  • 定义 state:在函数内的最外层,使用 useState 创建响应式数据和修改响应式数据的方法
  • 修改 state:调用第二参数修改响应式数据
  • state dom 更新后执行:借助 useEffect 实现依赖绑定
js
import React, { useState, useEffect, useRef } from 'react';
function MyComponent() {
  // 1、定义 基础类型 状态
  const [count, setCount] = useState(0);

  // 1、定义 对象类型 状态
  const [user, setUser] = useState({
    name: '张三',
    age: 20,
    email: 'zhangsan@example.com',
  });

  const countRef = useRef(null);

  const increment = () => {
    setCount(count + 1); // 2、修改 基础类型 状态

    // 2、修改 对象类型 状态
    setUser({
      ...user,
      age: 21,
    });
    // 2、修改状态(传入函数):可获取之前值,返回新值
    setCount((prevCount) => prevCount - 1);
  };

  // 3、当 count 变化且 DOM 更新后执行
  useEffect(() => {
    console.log('DOM已更新,count:', count, 'ref:', countRef.current);
  }, [count]);

  return <div ref={countRef}>{count}</div>;
}

在类组件中

  • 定义 state:在 constructor 中,使用 this.state 定义状态树,在状态树上定义响应式数据
  • 修改 state:调用 this.setState() 并以对象属性的方式传入改变的最新状态
  • state dom 更新后执行: this.setState() 的第二个参数为更新后回调
js
class ClassComponent extends React.Component {
  constructor(props) {
    super(props);
    // 1、在状态树上定义状态
    this.state = {
      count: 0,
      name: 'John',
    };
  }

  incrementCount = () => {
    // 2、修改状态 & 第二个参数为 dom 更新后执行回调
    this.setState(
      {
        count: this.state.count + 1,
      },
      () => {
        console.log('count 已更新为:', this.state.count);
      }
    );

    // 2、修改状态(传入函数):可获取之前值,返回新值,第二个参数为 dom 更新后执行回调
    this.setState(
      (prevState) => ({ count: prevState.count + 1 }),
      () => {
        console.log('count 已更新为:', this.state.count);
      }
    );
  };
}

setState 执行机制

  • setState 第一个参数可以是对象,可以是函数,第二个参数为回调函数在状态更新后执行
  • 当 setState 第一个参数为函数时:函数的第一个参数为最新 state,第二个参数为最新 props
  • 异步/同步与批量:多数情况下可理解为「更新会批量提交、不会立刻读到最新 state」。React 18 起默认 automatic batching,在 setTimeout、Promise、原生事件里多次 setState 也常合并为一次渲染;React 17 及以前在这些脱离合成事件的场景里更容易出现「看起来像同步刷新」的行为。需要强制同步提交时用 flushSync
  • 合并批量更新:在一个方法中多次执行 setState 效果是合并覆盖、不同的合并,相同的只有最后一个生效,相当于 Object.assign(可能造成同一个属性更新缺失)

state 增删改

使用 Immer、Immutable.js 或扩展运算符 ...,避免直接修改 state。

  1. 函数 state 操作
js
// 错误方式:直接修改原数组
arr.push(newItem);
setArr(arr);

// 正确方式:创建新数组
setArr([...arr, newItem]); // 添加
setArr(arr.filter(item => item.id !== id)); // 删除
setArr(arr.map(item => item.id === id ? {...item, done: true} : item)); // 更新
  1. 对象 state 操作
js
// 错误方式:直接修改原对象
obj.name = 'new name';
setObj(obj);

// 正确方式:创建新对象
setObj({...obj, name: 'new name'});
  1. hook 函数操作
js
// 避免依赖旧状态的多个更新互相覆盖
setCount(prev => prev + 1);

// 数组更新更安全
setItems(prev => [...prev, newItem]);

React setState 执行机制

回答

shell
setState React 类组件中用于更新状态、触发视图重新渲染的核心 API,其设计遵循 不可变数据 + 批量更新 原则

# setState 执行机制分为状态更新和渲染两个阶段
状态更新阶段:React 会将 setState 的更新请求入队并批量处理;子树是否跳过更新由 **`shouldComponentUpdate` / `PureComponent` / `React.memo`** 等在 **reconcile** 阶段决定(不是「入队时」单独一步)。
渲染触发阶段:状态更新完成后,React 会重新执行 render 方法,生成新的虚拟 DOM,通过 Diff 算法对比新旧虚拟 DOM,最终只更新变化的真实 DOM。

setState 执行机制拆解

shell
# 首先:异步执行(非绝对)
默认场景(合成事件 / 生命周期):setState “异步” —— 调用后不会立即更新 this.state,也不会立即触发渲染。React 会将多次 setState 合并,待当前同步代码执行完毕后,批量更新状态并触发一次渲染,目的是减少 DOM 操作、提升性能。
// 示例:合成事件中 setState 异步
handleClick = () => {
  this.setState({ count: this.state.count + 1 });
  console.log(this.state.count); // 输出旧值,未立即更新
};

**React 18 注意**:定时器 / Promise / 原生事件中的多次 `setState` 通常仍会被 **自动批处理**`this.state` 未必在同步代码里立刻变成新值;与 React 17 及以前的直觉不同。需要「立刻提交 DOM」时用 `flushSync` 包裹。
// 示例:定时器里连续 setState(18 下多会合并为一次渲染)
componentDidMount() {
  setTimeout(() => {
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 }); // 若未批处理/未用函数式,仍可能只基于旧 state 合并
    console.log(this.state.count); // 18 下多为旧值;用回调第二个参数或 componentDidUpdate 读最新值更可靠
  }, 0);
}

# 其次:批量更新
React 会维护一个更新队列,在同一事件循环中,多次调用 setState 会被合并为一次更新:
- 对象式 setState:后调用的会覆盖前调用的同字段(浅合并)
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 最终 count 只加 1(因为两次读取的都是旧 state)
- 函数式 setState:接收 prevState(上一次的真实状态)和 props,能保证状态更新的连续性,避免批量更新导致的覆盖问题。
this.setState(prevState => ({ count: prevState.count + 1 }));
this.setState(prevState => ({ count: prevState.count + 1 }));
// 最终 count 2(每次读取的是最新的 prevState)

# 最后:状态更新与渲染
状态更新:React 将更新入队并批量处理;子树是否 `render` **`shouldComponentUpdate` / `PureComponent` / `React.memo`** 等在 reconcile 阶段决定。
渲染触发:状态更新完成后,React 会重新执行 render 方法,生成新的虚拟 DOM,通过 Diff 算法对比新旧虚拟 DOM,最终只更新变化的真实 DOM。

# 拓展
- setState 的第二个参数:可选的回调函数,会在状态更新完成、组件渲染后执行,可用于获取最新状态(替代异步场景下的立即读取)。
this.setState({ count: 1 }, () => {
  console.log(this.state.count); // 输出 1,已更新
});
- React 18 变化:React 18 中默认开启 automatic batching(自动批量更新),即使在定时器、Promise 等场景,也会默认批量处理 setState,若需取消批量更新,可使用 ReactDOM.flushSync。
import { flushSync } from 'react-dom';
setTimeout(() => {
  flushSync(() => {
    this.setState({ count: 1 }); // 同步更新,立即触发渲染
  });
}, 0);

setState 是同步还是异步?更新机制?如何强制获取更新后状态?

回答

shell
# 总结
 **批量更新** 理解更准确:**React 18** 在多数场景(含 `setTimeout` / Promise / 原生事件)默认 **automatic batching****React 17 及以前**在脱离合成事件的场景里更容易出现「非批量、像同步」的表现。读取最新状态用 **setState 回调****componentDidUpdate** **`useEffect` 依赖**。

# 更新机制、原理
批量场景:React 合并同一轮中的更新,减少渲染。调用 `setState` 后立刻读 `this.state` 往往仍是旧值。
需强制同步刷新:使用 **`react-dom` `flushSync`**(会打断批处理,慎用)。

# 获取更新后状态
setState 的第二个参数(回调函数):setState ({count: 1}, () => console.log (this.state.count))。
useEffect 监听状态变化:useEffect (() => { ... }, [count])。

React:不可变数据(Immutability)

为什么强调不可变

React 依赖 引用变化 判断是否需要更新(PureComponentReact.memo、hooks 依赖数组)。若 原地修改(mutate) 对象或数组,引用不变,容易导致 跳过更新难以调试的状态异常

常见错误写法

js
state.items.push(newItem); // 禁止:修改原数组
setState(state);           // 引用未变

推荐写法

js
setItems([...items, newItem]);
setUser({ ...user, name: 'next' });

深层结构可使用 immer结构化克隆 或拆分状态降低嵌套。

与 Redux 的关系

Redux 要求 reducer 纯函数 + 不可变返回,便于时间旅行与预测状态;RTK 内置 immer 可写风格 reducer。

Immutable.js

持久化数据结构适合超大列表与复杂变更,但学习与互操作成本较高;多数项目 展开运算符 + Immer 已足够。


React render 方法原理?执行时机?

回答

shell
# render 方法的核心原理:
类组件的 render 是组件的核心生命周期方法,且必须是纯函数 —— 无副作用、不修改 state、相同输入(props/state)返回相同虚拟 DOM,这是 React 可预测性的基础;
render 返回的 React 元素是虚拟 DOM 的抽象描述,React 会经过 Reconciliation(调和)过程,通过 Fiber 架构(React 16+)将虚拟 DOM 转换为 Fiber 节点,再通过 Diff 算法对比 Fiber 树,计算出需要更新的部分(Effect List),最后通过 commit 阶段将差异应用到真实 DOM;
ReactDOM.render(React 18 前)是根组件的挂载入口,React 18 后替换为 createRoot,核心逻辑一致,但 createRoot 支持并发渲染,能优先级调度渲染任务,避免长任务阻塞主线程。

# render 的执行时机(区分类组件和函数组件):
- 类组件:
挂载阶段:constructor 之后、componentDidMount 之前执行一次;
更新阶段:state 变化(setState 即使值未变,默认触发,可通过 shouldComponentUpdate 拦截)、props 变化、父组件渲染、Context 变化、forceUpdate 调用;这里要注意,setState 是异步的,批量更新时会合并多次 setState,最终只触发一次 render;
- 函数组件:
没有 render 方法,组件本身就是渲染函数,重新执行等价于类组件的 render 调用;
触发重执行的时机包括:state/useReducer 状态更新、父组件重渲染(可通过 React.memo 浅比较 props 优化)、useContext 上下文更新、useCallback/useMemo 依赖变化(间接触发);
另外,React 18 的并发特性下,函数组件可能出现 “中断渲染”(比如高优先级任务插队),但由于是纯函数,不会产生副作用,这也是纯函数设计的优势。

最后补充优化点:无论是类组件还是函数组件,不必要的 render 会带来性能损耗,类组件可通过 shouldComponentUpdate PureComponent,函数组件通过 React.memo + useCallback/useMemo 来避免无意义的重渲染,核心是减少不必要的虚拟 DOM 对比。

细化

shell
# 核心原理:
类组件必须实现的核心方法,本质是一个纯函数、它的作用不是直接渲染 DOM,而是返回一个 React 元素(虚拟 DOM,JSX 语法糖编译后的产物),描述当前组件应该呈现的 UI 结构。
React 会将这个虚拟 DOM 与上一次的虚拟 DOM 进行对比(Diff 算法),计算出最小更新量,再将差异部分更新到真实 DOM 中(Reconciliation 调和过程)。

# 流程图解
触发 render -> 执行 render  -> 生成虚拟 dom -> React 使用 diff 算法 对比新旧 DOM -> 计算最小更新量 -> 更新真实 dom 

# 执行时机
初始化阶段:组件挂载时(componentDidMount 前),render 会执行一次,生成初始虚拟 DOM 并渲染真实 DOM。
更新阶段(核心,需重点说):
 组件自身 state 变化(调用 this.setState(),即使 state 值未变,默认也会触发 render,可通过 shouldComponentUpdate 阻止);
 父组件重新渲染时,子组件默认会继续 reconcile;若 props 浅比较相等且子为 **`React.memo`**,或类子 **`shouldComponentUpdate` 返回 `false`**,可跳过子 `render`
 组件接收的 props 发生变化;
 调用 this.forceUpdate()(强制触发,跳过 shouldComponentUpdate 检查);
 上下文(Context)的值发生变化,且组件消费了该上下文。
初始化挂载必执行,更新阶段由 state/props/ 父组件 / 上下文变化触发,函数组件无 render 方法,组件重执行等价于 render 调用;

# 函数组件(无 render 方法,但渲染逻辑等价于 return 部分)执行时机
初始化挂载时执行一次;
组件内 useState/useReducer 的状态更新;
父组件重新渲染(可通过 React.memo 优化);
依赖的 useContext 上下文更新;
useCallback/useMemo 的依赖项变化(间接触发)

React Refs 理解与使用场景

回答

shell
Refs React 提供的直接访问 DOM / 类组件实例的方式,useRef(函数组件)/createRef(类组件)创建,通过 current 属性访问目标;

# 注意事项
Refs 不能用在函数组件上(因为函数组件无实例),若需给函数组件绑定 ref,需用 forwardRef 转发 ref;
函数组件中,useRef createRef 更高效(createRef 每次渲染都会创建新的 ref 对象)

react 类组件 super

为什么要使用 super(props) 而不是 super()?

直接调用父类构造函数,不传递 props 给父类构造函数,在构造函数中可以通过 this.props 访问 props,官方不推荐 因为,在构造函数中 this.props 将是 undefined 在构造函数外(如 render 方法)仍可以访问 this.props,因为 React 会在之后自动设置

在 React 16.9 之前,如果需要在构造函数中访问 this.props,必须使用 super(props) 现代 React 版本中,React 会在构造函数调用后自动设置 props,所以技术上两者都可以工作 但为了代码清晰性和一致性,通常建议使用 super(props)

总结

  1. react 是基于 ES6 Class 类的,所以如果使用 constructor 函数,必须使用 super
  2. 无论有没有 constructor 函数,在 render 方法中,this.props 都是可以使用的,这是因为 React 内部实现了, 在构造函数之外自动设置了 this.props,但是不建议 super() 代替 super(props)

react 中 super 和 super(props)的区别

回答

shell
super() 和 super(props) 的核心区别在于是否能在组件实例的 constructor 中访问到 props
 ES6 的类中,super 是子类调用父类构造函数的关键字。React 类组件本质是继承了 React.Component
super():父类构造函数没收到 props,所以在 constructor 内部 this.props 是 undefined;
 render 等其他生命周期里 this.props 是正常的(React 后续会把 props 挂载到实例上)。
super(props):父类构造函数收到 props 并立即挂载到 this 上,所以 constructor 内就能直接用 this.props。
最后,无论是否需要在 constructor 中用 props,都建议写 super(props),避免潜在的边界问题,代码也更规范。

# 注意
1. 只要手动定义 constructor,第一步必须调用 super()(否则报错)。
2. 简化写法:不写 constructor 时的默认行为
如果你的组件不需要手动写 constructor,React 会默认帮你调用 super(props)
等价于写了 constructor(props) { super(props); }
class MyComponent extends React.Component {
  render() {
    return <div>{this.props.name}</div>;
  }
}

# 拓展
 React 16 之前的部分版本中,如果只写 super(),即使在 constructor 外的生命周期(如 componentWillMount)中,this.props 也可能出现短暂的 undefined;而 super(props) 能避免这个问题(现在 React 已修复,但最佳实践仍建议传 props)

constructor() 是组件的构造函数,只要手动写了 constructor,必须首先调用 super(),否则会报错(因为子类实例化时必须先完成父类的初始化)
React.Component 的构造函数本身会接收 props 参数,并把它挂载到实例上(即 this.props)