> ## Documentation Index
> Fetch the complete documentation index at: https://opc-43d279b8.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# React 版本演进史

> 从 FaxJS 到 React 19，梳理版本演进、Fiber 架构、事件系统与生命周期变迁。

> React 的版本演进可以按"每次重写解决了什么"来记：栈调和卡在哪、Fiber 补上了什么、Hooks 又换掉了哪种写法。

***

## Q1: React 的历史？为什么要有它？

```text theme={null}
┌────────────────┐   ┌────────────────┐   ┌────────────────┐   ┌────────────────┐   ┌────────────────┐
│ 2010 XHP+PHP   │ → │ 2011 FaxJS     │ → │ 2012 Instagram │ → │ 2013 开源发布  │ → │ 2015 React 15  │
│ UI 危机         │   │ 函数式原型      │   │ 内部验证       │   │ 虚拟 DOM 亮相   │   │ Stack Reconciler│
└────────────────┘   └────────────────┘   └────────────────┘   └────────────────┘   └────────────────┘
```

**1. 2010 年 - Facebook 的 UI 危机**

Facebook 当时用 PHP + XHP（PHP 的 XML 扩展）拼接 HTML DOM。通知系统是重灾区——用户收到一条新消息，需要手动查询 DOM 节点、判断是否已存在、再插入或更新。随着消息类型（点赞、评论、@提及）不断膨胀，代码变成一团意大利面条，状态与 DOM 严重耦合，修一个 bug 冒出三个新 bug。

**2. 2011 年 - Jordan Walke 的原型 FaxJS**

Facebook 工程师 Jordan Walke 受 XHP（Facebook 内部的 PHP XML 扩展框架）的启发——XHP 让开发者在 PHP 中用 XML-like 语法构建 UI 组件。Walke 将这个思想带入 JavaScript，构建了 FaxJS 原型。核心思想：**UI = f(state)**——给定一个状态，函数返回一棵描述 UI 结构的轻量对象树（即虚拟 DOM 的雏形）。状态变化时重新调用函数生成新树，框架自动 diff 两棵树的差异并更新真实 DOM。开发者从此不再手动操作 DOM。Walke 在 2019 年的演讲中回顾了这段经历，称 React 的设计灵感来自"函数式编程对 UI 的重新定义"\[1]。

**3. 2012 年 - Instagram 的验证**

Facebook 内部孵化出 Instagram 后，团队面临"从零构建一个完整 SPA"的需求。他们选择用 FaxJS 的思想来驱动 Instagram 的前端。这次内部验证证明了声明式 UI 的可行性——代码量大幅减少，bug 率显著降低，团队开始认真打磨这套方案。

**4. 2013 年 - React 开源（JSConf US）**

2013 年 5 月，JSConf US 上 Pete Hunt 做了"Rethinking Web App Development at Facebook"演讲，React 正式开源\[2]（Jordan Walke 是 React 的主要作者）。虚拟 DOM + JSX 的组合震惊了业界——"在 JavaScript 里写 HTML？"引发了巨大争议。Pete Hunt 在演讲中强调："React 不是在教你怎么做 UI，而是在教你如何思考 UI——把 UI 看成数据的函数输出。"Facebook 花了很长时间说服社区接受"关注点分离不是文件分离，而是组件分离"的理念。早期生态极不完善，没有路由、没有状态管理，但核心思想逐渐被验证。

**5. 2015 年 - React 15（Stack Reconciler）**

React 15 采用 Stack Reconciler 架构：用同步递归遍历虚拟 DOM 树做 diff。这个架构简单直接，在中小型应用中表现良好。但它的致命问题是**同步且不可中断**——一旦开始 diff，就必须一口气跑完整棵组件树，大型应用可能阻塞主线程 100ms+，导致掉帧和输入延迟。

> [!TIP]
>
> React 诞生之前的操作 DOM 就像用纸质账本管理仓库——每来一批货（新数据），你得手动翻账本（查询 DOM）、核对数量、涂改记录（修改 DOM），账本越厚越容易出错。React 就像引入了一套 ERP 系统——你只需要录入数据（声明状态），系统自动生成最新的库存报表（渲染 UI），再也不用手工对账了。

**React 诞生解决的核心问题：**

```text theme={null}
问题：手动操作 DOM + 状态与视图耦合 = 代码混乱、难以维护

新消息到达 → 手动查询 DOM → 判断节点是否存在 → 插入或更新 → 状态散落在各处

解决：声明式 UI — 状态驱动视图，框架自动 diff 更新

state 变化 → 调用 render() → 生成新虚拟 DOM → diff 对比 → 只更新变化的真实 DOM
```

> [!NOTE]
>
> React 的诞生源于 Facebook 的 UI 维护危机，Jordan Walke 用"UI = f(state)"的函数式思想创造了虚拟 DOM，让开发者从手动操作 DOM 的泥潭中解放出来。

> [card] 没有 React（2010 年之前）
>
> 前端用 jQuery 手动操作 DOM，状态散落在各种全局变量和 DOM 属性中。通知系统每来一条消息就要 `document.querySelector` + `if/else` 判断 + `appendChild`，就像一个仓库管理员没有任何清单系统，每次取货都要满仓库翻找。

> [card] 引入 React（2013 年之后）
>
> 声明式 UI 让开发者只需描述"UI 应该长什么样"，框架负责 diff 和更新。就像仓库引入了智能库存系统——管理员只需要说"我要 5 号货架上的东西"，系统自动定位、取货、更新库存记录。

> [!TIP]
>
> 回答时先说"为什么"（Facebook 的 UI 维护危机），再说"怎么解决"（声明式 UI + 虚拟 DOM）。想听的是你对技术诞生驱动力的理解，而不是背诵年份。

***

## Q2: React 15 的问题是什么？怎么解决？

### Stack Reconciler 的致命问题

React 15 的协调器（Stack Reconciler）采用同步递归算法遍历整棵组件树。当 `setState` 触发更新时，React 从根节点开始递归对比每个组件的虚拟 DOM，这个过程一旦开始就无法中断。

```text theme={null}
setState 触发
     │
     ▼
┌──────────────────────────────────────────┐
│  同步递归遍历整个组件树                      │
│  每个组件:  shouldComponentUpdate?         │
│            → render() 生成新虚拟 DOM        │
│            → diff 对比新旧虚拟 DOM          │
│            → 收集差异                       │
│                                           │
│  无法中断！必须一口气跑完！               │
│  大型应用: 100ms+ 阻塞主线程               │
└──────────────────────────────────────────┘
     │
     ▼
  commit 更新到真实 DOM
```

核心问题有三个：

1. **同步递归无法中断**：JavaScript 的调用栈是后进先出结构，递归过程中无法在中间状态暂停并恢复。大型应用一次更新可能占用主线程 100ms+，直接导致掉帧。
2. **没有优先级概念**：所有更新一视同仁——用户输入和数据加载的更新享有相同优先级，无法优先响应用户交互。
3. **大型组件树性能悬崖**：组件数量越多，递归深度越深，性能呈线性下降，且没有优雅的降级方案。

### React 15 的优化尝试

面对这些问题，React 15 时期社区和官方提供了三种优化手段：

**1. shouldComponentUpdate（手动中断递归）**

```javascript theme={null}
class ListItem extends React.Component {
  shouldComponentUpdate(nextProps) {
    // 如果数据没变，跳过该子树的 diff
    return nextProps.item.id !== this.props.item.id;
  }
  render() {
    return <div>{this.props.item.name}</div>;
  }
}
```

`shouldComponentUpdate` 返回 `false` 时，React 会跳过该组件及其整个子树的 diff。但问题是：开发者必须手动判断每个 props 是否变化，遗漏任何一个就会导致不必要的渲染，写错了还会导致 UI 不更新。

**2. PureComponent（浅比较自动化）**

```javascript theme={null}
class ListItem extends React.PureComponent {
  render() {
    return <div>{this.props.item.name}</div>;
  }
}
```

`React.PureComponent` 自动对 props 和 state 做浅比较（shallow compare），省去了手写 `shouldComponentUpdate` 的麻烦。但浅比较有天然缺陷——如果父组件传入新的数组或对象引用（即使内容相同），浅比较会认为"变了"，导致无效渲染。

**3. Batching（批处理合并更新）**

```javascript theme={null}
// React 事件回调内：自动批处理
handleClick() {
  this.setState({ a: 1 });
  this.setState({ b: 2 });
  // 只触发一次渲染
}

// setTimeout 内：不批处理！
handleClick() {
  setTimeout(() => {
    this.setState({ a: 1 });
    this.setState({ b: 2 });
    // 触发两次渲染！
  }, 0);
}
```

React 15 的批处理仅限于 React 自己管理的事件回调。在 `setTimeout`、`Promise` 等原生异步上下文中，每次 `setState` 都会立即触发一轮完整渲染。

### 为什么这些优化不够？

```text theme={null}
shouldComponentUpdate  →  手动维护成本高，容易出错
PureComponent          →  浅比较无法处理复杂数据结构
Batching               →  只在 React 事件内生效，异步上下文无效

本质问题：这些优化都在"减少工作量"，但没有解决"工作方式"的根本缺陷

Stack Reconciler 依然是同步递归 → 依然一口气跑完 → 依然阻塞主线程
```

> [!TIP]
>
> React 15 的 Stack Reconciler 就像一个必须一口气跑完全程的马拉松选手——`shouldComponentUpdate` 相当于给选手"抄近路"的机会，`PureComponent` 相当于"自动识别近路"，`Batching` 相当于"把多次出发合并成一次"。但无论怎么优化，选手依然不能中途停下来休息。React 16 的 Fiber 就像换了新规则——每跑 100 米就可以在路边歇一歇（时间切片），休息够了再接着跑，从此不再怕大型应用的"马拉松"。

> [!NOTE]
>
> React 15 的 Stack Reconciler 用同步递归做 diff，`shouldComponentUpdate`、`PureComponent` 和 Batching 只能"减少工作量"，无法改变"必须一口气跑完"的根本限制——React 16 的 Fiber 架构才是真正的解法。

***

## Q3: React 18 的并发模式是如何工作的？

React 18 最核心的突破是将并发特性（Concurrent Features）从实验阶段正式落地。React 官方博客对并发能力的描述是：不必在"快速响应用户输入"和"渲染大量数据"之间二选一\[3]。它的本质是赋予 React **中断并优先处理更重要渲染任务**的能力。

```text theme={null}
┌───────────────────────────────────────────────┐
│              浏览器主线程                        │
└──────┬───────────────────────────────┬────────┘
       │                               │
       ▼                               ▼
┌──────────────┐                ┌──────────────┐
│  低优任务 (A)  │                │ 高优任务 (B)  │
│ 渲染大列表     │                │ 用户输入框打字 │
└──────┬───────┘                └──────┬───────┘
       │                               │
       │  (执行一半，耗时 5ms)           │
       ├───────────────────────────────┤  ← React 调度器发现高优交互
       │  中断当前 A 任务的渲染            │
       │                               │
       │                               ▼
       │                        ┌──────────────┐
       │                        │ 执行 B 任务   │
       │                        │ 更新输入框 UI  │
       │                        └──────┬───────┘
       │                               │
       │  (B 任务执行完毕)                │
       ├───────────────────────────────┘  ← 恢复现场
       ▼
┌──────────────┐
│  恢复任务 (A)  │
│ 继续渲染大列表 │
└──────────────┘
```

```tsx theme={null}
// 并发更新的核心 API
import { useState, useDeferredValue, useTransition } from "react";

// 方式一：useDeferredValue，适合值来自 props、无法改更新调用方的场景
function SearchList({ allItems }) {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  const filtered = allItems.filter((item) => item.name.includes(deferredQuery));

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul style={{ opacity: isStale ? 0.5 : 1 }}>
        {filtered.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </>
  );
}

// 方式二：startTransition，适合自己能控制 setState 的场景
function FilterPanel({ allItems }) {
  const [list, setList] = useState(allItems);
  const [isPending, startTransition] = useTransition();

  const handleFilter = (keyword) => {
    // 非紧急更新：可以被高优先级更新打断
    startTransition(() => {
      setList(allItems.filter((item) => item.name.includes(keyword)));
    });
  };

  return (
    <button onClick={() => handleFilter("react")} disabled={isPending}>
      {isPending ? "过滤中..." : "只看 React"}
    </button>
  );
}
```

> [!WARNING]
>
> React 18 默认启用并发渲染能力，但不会把所有更新都当成低优先级。只有显式调用 `startTransition` 或使用 `useDeferredValue`，某个更新才会被标记为"非紧急任务"；普通 `setState` 走 DefaultLane，仍按普通优先级调度。

> [!TIP]
>
> 并发模式就像医院急诊室的分诊机制。你正在给一个脚踝扭伤的病人（低优的列表渲染）做包扎，突然推车送来一个心脏病发的重症患者（高优的用户输入）。医生（React 调度器）会立即暂停包扎，优先抢救心脏病患者，等抢救结束脱离危险后，再回去把扭伤病人的绷带打完。

***

## Q4: React 16 为什么是一次架构革命？

> [!TIP]
>
> React 16 的架构革命就像工厂流水线的升级。React 15 的流水线一旦开工就不能停——哪怕工人（JS 引擎）已经累得干不动了，也得一口气把所有工序做完（同步递归），产品（UI）才能下线。React 16 把流水线改成了"可暂停"的模式——每做完一道工序（一个 Fiber 节点），就检查一下"下班时间到了没"（shouldYield），到了就暂停，把半成品存好（保存 nextUnitOfWork），明天接着干。同时 Hooks 的引入就像把分散在不同车间的工具（状态、副作用）集中到了一个工具箱里（函数组件），取用更方便。

**1. 16.6ms 的帧预算与同步阻塞**

现代浏览器为了保持 60fps，要求每帧执行时间在 16.6ms 以内。React 15 的 Stack Reconciler 用同步递归对比虚拟 DOM，一旦开始就无法中断。大型应用一次 `setState` 可能占用主线程 100ms+，直接导致掉帧卡顿。

**2. Stack Reconciler 的致命局限**

JavaScript 原生调用栈（Call Stack）是后进先出的结构，无法在中间状态暂停并事后恢复。这意味着 React 15 没有任何办法把一个巨大的更新任务拆成小块，只能一口气跑完。

**3. Fiber 架构的引入**

React 16 彻底抛弃递归算法，引入了基于单链表的 Fiber 架构。每个组件对应一个 Fiber 节点，通过 `child`、`sibling`、`return` 三个指针构建虚拟调用栈，用 `while` 循环替代递归，实现了时间切片（Time Slicing）。

**4. 生命周期的废弃与重塑**

由于 Fiber 允许 Render 阶段的中断和重新执行，旧版带有副作用的生命周期（`componentWillMount`、`componentWillReceiveProps`、`componentWillUpdate`）可能被多次触发。React 从 16.3 起标记废弃，并在 React 17 移除，引入 `getDerivedStateFromProps` 等更纯粹的替代方案。

\*\*5. Hooks 的范式转移 (16.8) \*\*

Hooks 让函数组件拥有了状态管理和副作用处理的能力，逻辑复用从高阶组件（HOC）和 Render Props 的地狱中解放出来。`useState`、`useEffect`、`useMemo` 等 API 极大降低了代码耦合度。

**React 16 解决的核心问题：**

```text theme={null}
问题：同步递归 + 无法中断 = 大型应用卡顿

setState → 同步递归遍历整个组件树 → 主线程被阻塞 100ms → 掉帧卡顿

解决：Fiber 时间切片 — 拆分成微小执行单元，每执行一个就检查剩余时间

setState → 执行一个 Fiber 节点 → 检查时间 → 有剩余继续，没剩余让出主线程 → 下一帧恢复
```

> [!NOTE]
>
> React 16 用 Fiber 链表替代递归调用栈，把"一口气跑完"变成了"跑一段歇一下"，彻底解决了大型应用的主线程阻塞问题。

> [card] React 15 的痛点
>
> 同步递归对比虚拟 DOM，阻塞主线程无法中断；逻辑复用靠高阶组件层层嵌套。就像一个必须一口气跑完全程的马拉松，中途不能停下调整呼吸。

> [card] React 16 的破局
>
> Fiber 时间切片实现可中断渲染，Hooks 实现函数式逻辑复用，Error Boundaries 实现优雅降级。就像修了服务区的接力赛跑，可以停顿、可以重试、可以交接。

> [!TIP]
>
> 回答时先说"为什么"（同步递归阻塞主线程），再说"怎么解决"（Fiber 时间切片）。想听的是你对架构演进背后驱动力的理解，而不是流水账背版本号。

***

## Q5: React 16 vs 18 vs 19 有哪些关键差异？

> [!TIP]
>
> React 16 就像第一代智能手机——终于能中断任务了（Fiber 时间切片），但还不知道该优先处理什么。React 18 就像第二代——有了智能分屏（并发模式），可以一边看视频一边回消息（高优任务打断低优任务），而且所有应用的通知都统一管理了（Automatic Batching）。React 19 就像第三代——有了 AI 助手（React Compiler），它能自动帮你整理相册（自动 memoization），你再也不用手动分类了。

### 版本演进的底层逻辑

React 每次大版本升级都不是孤立的 API 增加，而是在解决前一版本遗留的架构瓶颈。理解这个演进逻辑，比记住版本号更重要。

### 基础梳理

| 维度 | React 16 | React 18 | React 19 |
| - | - | - | - |
| **渲染架构** | Fiber（可中断） | 并发渲染（Concurrent） | 并发渲染 + 可选 React Compiler（独立构建插件） |
| **批处理** | 仅 React 事件内批处理 | 自动批处理（含 setTimeout/Promise） | 同 18，编译器可辅助减少重渲染 |
| **SSR** | renderToString | 流式 SSR + Suspense | Server Components 一等公民 |
| **状态管理** | useState/useReducer | + useTransition/useDeferredValue | + use() hook + Actions |
| **事件系统** | 代理到 document | 代理到 Root Node | 同 18 |

### 专家视角深度剖析

很多人只能背诵上面的表格，但实际更在意的是：**React 每次大版本升级背后的技术驱动力是什么？**

React 16 的核心驱动力是**解决同步阻塞**。Fiber 架构把递归变成迭代，为时间切片奠定了基础。但 Fiber 只是"能中断"，并没有告诉 React "该中断什么、优先处理什么"。

React 18 补上了这块拼图。Andrew Clark 在 Lanes 设计 PR 中说明了从 ExpirationTime 到 Bitmask 的演进动机：ExpirationTime 用一个数字表示优先级，只能做线性比较；Lanes 用位图表示多个优先级通道，可以通过位运算快速判断任务批次和插队关系\[4]。这让调度器可以根据任务类型（用户输入 > 过渡动画 > 数据获取）决定中断策略。同时，自动批处理（Automatic Batching）解决了 React 17 中 `setTimeout` 和 `Promise` 内 `setState` 不批处理的历史包袱。

React 19 的方向是**减少手写优化与异步样板**。Sebastian Markbage 在 React Summit 2020 的演讲中阐述了 React Server Components 的设计动机：传统 React 应用把所有代码发给客户端；Server Components 只发送客户端需要的代码，同时直接访问后端资源\[5]。需要区分的是，React Compiler（原 React Forget）是独立构建插件，不是 React 19 运行时内置特性；Actions 和 `use()` 才是 React 19 正式提供的能力。`use()` 统一了 Promise、Context 的读取方式，Actions 简化了异步状态与表单处理。

```tsx theme={null}
// React 19 的 use() hook — 统一资源消费
function UserProfile({ userPromise }) {
  // use() 可以在条件语句和循环中使用（打破 Hooks 规则）
  // 注意：Promise 要来自缓存或上层，不能在渲染里直接新建，否则每次渲染都会重新请求
  const user = use(userPromise); // 自动与 Suspense 配合
  return <div>{user.name}</div>;
}

// React 19 的 Actions — startTransition 支持 async 函数
function UpdateName() {
  const [isPending, startTransition] = useTransition();
  const [error, setError] = useState(null);

  const handleSubmit = (name) => {
    startTransition(async () => {
      const error = await updateName(name);
      if (error) {
        setError(error);
        return;
      }
      redirect("/profile");
    });
  };
}
```

***

## Q6: React 事件系统为什么从 document 改到了 Root Node？

> [!TIP]
>
> React 16 的事件委托就像在一栋大楼门口装了一个大喇叭——所有楼层的所有事件都通过这一个喇叭广播（代理到 document），如果大楼里同时有两个公司（微前端多版本共存），两家的广播会互相干扰。React 17+ 改成了每家公司配一个对讲机（代理到各自 Root Node），各自收各自的信号，互不干扰。

### 合成事件与事件委托

React 的合成事件系统是其跨平台能力的基石——通过在顶层统一代理所有 DOM 事件，React 可以在不同渲染器（DOM、React Native）中提供一致的事件模型。

**React 16 的事件委托：**

React 16 将所有事件统一代理到全局 `document` 上。这在单应用时代没问题，但微前端场景下，多个 React 版本共存时，事件冒泡顺序会变得不可预测。

**React 17 的改变：**

从 React 17 开始，事件不再代理到 `document`，而是代理到该 React 应用渲染的根节点（Root Node，通常是 `div#root`）。这支持了多个 React 版本在同一页面共存。

```javascript theme={null}
// React 16：事件代理到 document
document.addEventListener("click", (e) => {
  // 所有 React 应用的点击事件都在这里处理
  dispatchEvent(e); // React 内部的合成事件分发
});

// React 17+：事件代理到 root node
rootNode.addEventListener("click", (e) => {
  // 只处理该 React 应用的事件
  dispatchEvent(e);
});
```

**关键区别：**

| 场景 | React 16 | React 17+ |
| - | - | - |
| 事件代理节点 | document | Root Node |
| 微前端多版本共存 | 事件冲突 | 各自独立 |
| stopPropagation 影响范围 | 影响所有 React 应用 | 只影响当前应用 |

> [!TIP]
>
> 微前端项目中，如果父子应用使用不同 React 版本，要注意事件冒泡顺序的变化。React 18 子应用的 `onClick` 会在 Root Node 层触发，之后才冒泡到 `document` 触发父应用 React 16 的监听器。

***

## Q7: React 生命周期经历了哪些变化？

> [!TIP]
>
> React 15 的生命周期就像一部严格按剧本演出的舞台剧——每个演员（组件）在固定的时间点说固定的台词（componentWillMount、componentDidMount 等），顺序绝对不能乱。React 16 的 Fiber 打破了这个规则——导演（调度器）可能会喊"停！这段重来！"（Render 阶段中断重试），所以演员不能在排练阶段（Render）做不可逆的事（副作用）。Hooks 时代更像即兴表演——没有固定剧本，演员根据当前场景（依赖数组）灵活决定什么时候行动（useEffect），同一个表演可以同时涵盖"开场、中场互动、谢幕"（一个 useEffect 覆盖多个生命周期）。

### 生命周期演进的底层逻辑

React 生命周期的每一次变化都不是孤立的 API 调整，而是底层架构变革的必然结果。理解这个演进逻辑，比记住每个生命周期方法的调用时机更重要。

### React 15 生命周期

React 15 的生命周期围绕类组件的挂载、更新和卸载三个阶段设计，整体流程是同步且线性的：

```text theme={null}
挂载阶段（Mounting）                 更新阶段（Updating）                 卸载阶段（Unmounting）
┌───────────────────┐            ┌───────────────────┐              ┌───────────────────┐
│ componentWillMount │            │ componentWillReceiveProps│         │ componentWillUnmount│
│        ↓          │            │        ↓           │              └───────────────────┘
│   render()        │            │ shouldComponentUpdate│
│        ↓          │            │        ↓           │
│ componentDidMount │            │ componentWillUpdate │
└───────────────────┘            │        ↓           │
                                 │   render()         │
                                 │        ↓           │
                                 │ componentDidUpdate │
                                 └───────────────────┘
```

```javascript theme={null}
class UserProfile extends React.Component {
  // 1. 挂载：组件即将插入 DOM（已被废弃）
  componentWillMount() {
    console.log("componentWillMount - DOM 不存在，不能做 DOM 操作");
  }

  // 2. 挂载：组件已插入 DOM
  componentDidMount() {
    console.log("componentDidMount - 可以安全地做 DOM 操作和数据请求");
    this.fetchUser(this.props.userId);
  }

  // 3. 更新：父组件传入新 props（已被废弃）
  componentWillReceiveProps(nextProps) {
    if (nextProps.userId !== this.props.userId) {
      this.setState({ loading: true });
    }
  }

  // 4. 更新：判断是否需要重新渲染
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.userId !== this.props.userId;
  }

  // 5. 更新：即将开始重新渲染（已被废弃）
  componentWillUpdate(nextProps, nextState) {
    console.log("componentWillUpdate - render 之前");
  }

  // 6. 更新：渲染完成后
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.userId !== this.props.userId) {
      this.fetchUser(this.props.userId);
    }
  }

  // 7. 卸载：组件即将从 DOM 移除
  componentWillUnmount() {
    console.log("componentWillUnmount - 清理定时器、取消订阅");
  }

  render() {
    return <div>{this.state.user?.name}</div>;
  }
}
```

### React 16 生命周期

React 16 引入 Fiber 架构后，Render 阶段可以被中断和重新执行。如果生命周期方法有副作用（如修改 state、操作 DOM），就会在中断恢复时被重复触发，导致不可预测的 bug。因此 React 16.3 废弃了三个带副作用的生命周期，引入了两个更"纯粹"的替代方案。

```text theme={null}
挂载阶段（Mounting）                 更新阶段（Updating）                 卸载阶段（Unmounting）
┌───────────────────┐            ┌───────────────────┐              ┌───────────────────┐
│  constructor()    │            │ getDerivedStateFromProps│         │ componentWillUnmount│
│        ↓          │            │        ↓           │              └───────────────────┘
│ getDerivedStateFromProps│       │ shouldComponentUpdate│
│        ↓          │            │        ↓           │
│   render()        │            │   render()         │
│        ↓          │            │        ↓           │
│ getSnapshotBeforeUpdate│ ← 新增│ getSnapshotBeforeUpdate│ ← 新增
│        ↓          │            │        ↓           │
│ componentDidMount │            │ componentDidUpdate │
└───────────────────┘            └───────────────────┘
```

**新增的两个生命周期：**

```javascript theme={null}
class ChatList extends React.Component {
  // 替代 componentWillMount + componentWillReceiveProps
  // 纯函数：只根据 props/state 计算返回值，没有副作用
  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.userId !== prevState.prevUserId) {
      return {
        prevUserId: nextProps.userId,
        messages: [], // 重置消息列表
      };
    }
    return null; // 不需要更新 state
  }

  // 替代 componentWillUpdate
  // 在 DOM 更新前获取快照信息（如滚动位置），传递给 componentDidUpdate
  getSnapshotBeforeUpdate(prevProps, prevState) {
    if (prevProps.messages.length < this.props.messages.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop; // 返回滚动偏移量
    }
    return null;
  }

  // 收到 getSnapshotBeforeUpdate 的返回值作为 snapshot 参数
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot; // 恢复滚动位置
    }
  }

  render() {
    return <div ref={this.listRef}>{/* 消息列表 */}</div>;
  }
}
```

### React 15 vs 16 生命周期对比表

| 阶段 | React 15 | React 16 | 变化原因 |
| - | - | - | - |
| 挂载 - 初始化 | `componentWillMount` | `constructor()` | `componentWillMount` 中 setState 不会触发额外渲染，直接用 constructor 替代 |
| 挂载 - 初始化 | `componentWillMount` | `getDerivedStateFromProps` | 纯函数，无副作用，不会被中断重复触发 |
| 更新 - props 变化 | `componentWillReceiveProps` | `getDerivedStateFromProps` | 挂载和更新统一用同一个静态方法 |
| 更新 - 渲染前 | `componentWillUpdate` | `getSnapshotBeforeUpdate` | Commit 阶段执行，只读 DOM 快照，不修改 state |
| 更新 - 渲染后 | `componentDidUpdate` | `componentDidUpdate` | 无变化，已在 Commit 阶段，安全 |
| 卸载 | `componentWillUnmount` | `componentWillUnmount` | 无变化，已在 Commit 阶段，安全 |

### 为什么 React 16 废弃了旧生命周期？

核心原因在于 Fiber 架构将渲染分为两个阶段：

```text theme={null}
Render 阶段（可中断）                Commit 阶段（不可中断）
┌──────────────────────────┐      ┌──────────────────────────┐
│ getDerivedStateFromProps  │      │ componentDidMount        │
│ shouldComponentUpdate     │      │ componentDidUpdate        │
│ render                    │      │ getSnapshotBeforeUpdate  │
│                           │      │ componentWillUnmount     │
│ 可能执行多次！          │      │ 只执行一次             │
│ 中断 → 丢弃 → 重新开始     │      │ 一旦开始就不会中断        │
└──────────────────────────┘      └──────────────────────────┘
```

`componentWillMount`、`componentWillReceiveProps`、`componentWillUpdate` 都处于 Render 阶段。如果在这些方法中有副作用（发起网络请求、修改全局变量、手动修改 DOM），中断恢复时副作用会被重复执行——发两次请求、改两次 DOM、导致数据错乱。

React 16 的策略是：Render 阶段只允许纯计算（`getDerivedStateFromProps` 返回新 state、`getSnapshotBeforeUpdate` 返回快照），所有副作用全部放到 Commit 阶段（`componentDidMount`、`componentDidUpdate`）。

### Hooks 生命周期

Hooks 时代没有显式的生命周期方法，但 `useEffect` 和 `useLayoutEffect` 覆盖了所有生命周期场景：

```javascript theme={null}
import { useEffect, useLayoutEffect } from "react";

function UserProfile({ userId }) {
  // 等价于 componentDidMount + componentDidUpdate + componentWillUnmount
  useEffect(() => {
    console.log("组件挂载或 userId 变化后执行");
    fetchUser(userId);

    // 返回的清理函数等价于 componentWillUnmount
    // 且在下次 effect 执行前也会先清理上一次的
    return () => {
      console.log("清理上一次 effect 或组件卸载");
    };
  }, [userId]); // 依赖数组：只有 userId 变化时才重新执行

  // useLayoutEffect vs useEffect 的区别：
  // useEffect   - 浏览器绘制后异步执行（不阻塞渲染）
  // useLayoutEffect - DOM 更新后、浏览器绘制前同步执行（阻塞渲染）
  useLayoutEffect(() => {
    // 适合读取 DOM 布局并同步修改（避免闪烁）
    const rect = ref.current.getBoundingClientRect();
    setTooltipPosition(rect.top);
  }, []);

  return <div ref={ref}>...</div>;
}
```

**useEffect vs useLayoutEffect 执行时机：**

```text theme={null}
setState 触发
     │
     ▼
React 计算新虚拟 DOM
     │
     ▼
更新真实 DOM
     │
     ├──────────────────────────────────┐
     ▼                                  ▼
useLayoutEffect（同步，阻塞绘制）    浏览器绘制（Paint）
     │                                  │
     ▼                                  ▼
  执行完毕                          useEffect（异步，不阻塞绘制）
     │                                  │
     ▼                                  ▼
浏览器绘制                         执行完毕
```

> [!WARNING]
>
> * `useEffect` 在浏览器完成绘制后异步执行，不会阻塞用户看到画面。适合大多数副作用（数据请求、日志、订阅）。
> * `useLayoutEffect` 在 DOM 更新后、浏览器绘制前同步执行。适合需要读取 DOM 布局并同步修改的场景（如定位 tooltip）。如果误用 `useLayoutEffect` 做耗时操作，会阻塞渲染导致页面卡顿。
> * 测试环境（Jest + jsdom）中 `useLayoutEffect` 会报 warning，因为测试环境没有真正的浏览器绘制。

> [!NOTE]
>
> React 生命周期从 15 的同步线性模型演进到 16 的可中断双阶段模型，本质原因是 Fiber 允许 Render 阶段中断重试，副作用必须全部移到 Commit 阶段；Hooks 用 `useEffect` 统一了生命周期概念，用依赖数组控制执行时机。

### Function Component vs Class Component

### 范式转移的本质

React 组件经历了从 Class Component 到 Function Component 的范式转移。Dan Abramov 在 Hooks RFC 中指出："Classes are confusing for both people and machines — `this` binding can be confusing, and there's no reliable way to extract reusable behavior between components"\[1]。理解两者的本质区别，是理解 React 为什么最终拥抱 Hooks 的前提。

**两种组件的本质区别：**

| 维度 | Class Component | Function Component |
| - | - | - |
| **本质** | 一个 ES6 Class 实例，有生命周期方法 | 一个纯函数，接收 props 返回 JSX |
| **状态管理** | `this.state` + `this.setState` | `useState` / `useReducer` |
| **副作用** | 生命周期方法（`componentDidMount` 等） | `useEffect` / `useLayoutEffect` |
| **逻辑复用** | HOC / Render Props（层层嵌套） | 自定义 Hook（扁平组合） |
| **this 绑定** | 需要手动处理 `this` 指向问题 | 没有 `this`，不存在绑定问题 |
| **代码量** | 样板代码多（constructor、bind、生命周期） | 样板代码少，更简洁 |
| **可测试性** | 需要实例化类，测试生命周期交互 | 纯函数入参出参，天然易测 |

**为什么 Class Component 被淘汰？**

```text theme={null}
Class Component 的三大痛点：

1. this 绑定地狱
   constructor 里 bind、render 里 bind、onClick 里箭头函数……
   一个组件 10 个事件处理函数 = 10 次 bind

2. 逻辑复用困难
   想复用"监听窗口大小"的逻辑 → HOC 包一层
   想复用"鼠标位置追踪"的逻辑 → 再包一层
   → "包装地狱"（Wrapper Hell）

3. 生命周期方法不内聚
   componentDidMount 里发请求 + 订阅 + 设置定时器
   componentWillUnmount 里清理订阅 + 清理定时器
   同一个逻辑分散在两个生命周期方法中 → 难以理解和维护
```

**Function Component 的优势：**

```javascript theme={null}
// Class Component：同一个逻辑分散在多个生命周期中
class WindowWidth extends React.Component {
  state = { width: window.innerWidth };

  componentDidMount() {
    window.addEventListener("resize", this.handleResize);
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.handleResize);
  }

  handleResize = () => {
    this.setState({ width: window.innerWidth });
  };

  render() {
    return <div>窗口宽度: {this.state.width}</div>;
  }
}

// Function Component：同一个逻辑集中在一处
function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return <div>窗口宽度: {width}</div>;
}

// 逻辑复用：提取为自定义 Hook，任何组件都能用
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return width;
}

// 使用：一行代码复用逻辑
function Dashboard() {
  const width = useWindowWidth();
  return <div>当前宽度: {width}</div>;
}
```

> [!NOTE]
>
> Class Component 用 Class 实例承载状态和生命周期，但 this 绑定、逻辑复用和生命周期分散是三大痛点；Function Component 用纯函数 + Hooks 实现相同能力，代码更简洁、逻辑更内聚、复用更扁平——这就是 React 16.8 引入 Hooks 后 Class Component 逐渐被淘汰的根本原因。

### 为什么要写 this.handleClick = this.handleClick.bind(this)

### JavaScript this 绑定与 React 的碰撞

Class Component 中最经典的"坑"之一：事件处理函数中的 `this` 丢失问题。这道题考察的是你对 JavaScript `this` 绑定规则的理解深度。

**Class Component 的 this 丢失问题：**

```javascript theme={null}
class Button extends React.Component {
  handleClick() {
    console.log(this); // undefined！this 丢失了
  }

  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}
```

**为什么 `onClick={this.handleClick}` 会丢失 this？**

```text theme={null}
关键：this 的值由"调用方式"决定，不是由"定义方式"决定

render() 中：
  <button onClick={this.handleClick}>
                    ↑
                    这只是"引用"了 handleClick 函数，并没有调用它

用户点击按钮时：
  浏览器调用 handleClick()
  ↑ 注意：是"直接调用"，不是"通过对象调用"

  直接调用 → this = undefined（严格模式下）
  就像：const fn = obj.method; fn(); // this 不是 obj
```

打个比方：你把名片（函数引用）给了浏览器，浏览器拿着名片打电话（调用函数）时，它不知道名片是谁给的——`this` 的指向在调用时才确定，而不是定义时。

**3 种解决方案：**

**方案 1：constructor 中 bind（经典方案）**

```javascript theme={null}
class Button extends React.Component {
  constructor(props) {
    super(props);
    // bind 返回一个新函数，this 永远绑定到实例
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log(this); // Button 实例
  }

  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}
```

**方案 2：箭头函数（在 render 中）**

```javascript theme={null}
class Button extends React.Component {
  handleClick() {
    console.log(this); // Button 实例
  }

  render() {
    // 箭头函数没有自己的 this，继承外层（render 的 this = 实例）
    return <button onClick={() => this.handleClick()}>Click</button>;
  }
}
```

> [!WARNING]
>
> 每次 render 都会创建一个新的匿名函数，导致 `React.memo` 和 `shouldComponentUpdate` 的浅比较失效（引用不同）。在列表渲染中尤其明显——100 个列表项 = 100 个新函数 = 子组件全部重新渲染。

**方案 3：类属性 + 箭头函数（推荐方案）**

```javascript theme={null}
class Button extends React.Component {
  // 类属性语法：箭头函数定义在实例上，this 自动绑定
  handleClick = () => {
    console.log(this); // Button 实例
  };

  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}
```

**错误与正确写法对比：**

```javascript theme={null}
// 错误：直接传递方法引用，this 丢失
class Counter extends React.Component {
  state = { count: 0 };

  increment() {
    this.setState({ count: this.state.count + 1 });
    // TypeError: Cannot read property 'setState' of undefined
  }

  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}

// 正确：constructor bind
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.increment = this.increment.bind(this);
  }

  state = { count: 0 };

  increment() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}

// 正确：类属性箭头函数（最简洁）
class Counter extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}
```

> [!NOTE]
>
> Class Component 中 `onClick={this.handleClick}` 丢失 this 的本质是 JavaScript 的"隐式绑定"在直接调用时失效；三种解决方案中，constructor bind 是经典方案、render 中箭头函数有性能隐患、类属性箭头函数最简洁且性能最优——而在 Function Component 中这个问题根本不存在，因为没有 `this`。

### constructor / props / super 的作用

### ES6 Class 继承与 React 的交互

Class Component 的 `constructor`、`props` 和 `super` 是面试中被问烂的基础题，但很多人只能背"要调用 super(props)"，却说不清"为什么"。这道题考察的是你对 JavaScript Class 继承机制的理解。

**constructor 的作用：**

```text theme={null}
constructor 是 ES6 Class 的构造方法，在实例被创建时自动调用。
在 React Class Component 中，constructor 有 3 个职责：

1. 初始化 state
2. 绑定事件处理函数的 this
3. 调用 super(props) 初始化父类（React.Component）
```

```javascript theme={null}
class UserProfile extends React.Component {
  constructor(props) {
    super(props); // 必须第一行调用

    // 职责 1：初始化 state
    this.state = {
      name: props.name,
      loading: false,
    };

    // 职责 2：绑定 this
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit() {
    console.log(this.state.name); // this 正确指向实例
  }

  render() {
    return <div>{this.state.name}</div>;
  }
}
```

**props 是什么？**

```text theme={null}
props = properties（属性）

props 是父组件传给子组件的只读数据。
子组件不能修改自己的 props——这是 React 单向数据流的核心原则。
```

```javascript theme={null}
// 父组件传递 props
function App() {
  return <UserProfile name="张三" age={25} />;
}

// 子组件接收 props
class UserProfile extends React.Component {
  render() {
    // this.props 是只读的
    return (
      <div>
        <p>姓名: {this.props.name}</p> {/* "张三" */}
        <p>年龄: {this.props.age}</p> {/* 25 */}
      </div>
    );
  }
}

// 子组件不能修改 props
class UserProfile extends React.Component {
  handleClick() {
    this.props.name = "李四"; // 报错！Props are read-only
  }
}
```

**为什么必须调用 super(props)？**

```text theme={null}
class UserProfile extends React.Component { ... }
                ↑
                继承了 React.Component

JavaScript 规则：子类的 constructor 中，必须先调用 super() 才能使用 this。
因为 super() 负责调用父类的构造函数来初始化 this 对象。

如果在 super() 之前访问 this → ReferenceError: Must call super constructor
```

```javascript theme={null}
// 不调用 super()
class Button extends React.Component {
  constructor(props) {
    // 没有调用 super()
    this.state = { clicked: false };
    // ReferenceError: Must call super constructor
    // before accessing 'this' or returning from derived constructor
  }
}

// 调用了 super() 但没传 props
class Button extends React.Component {
  constructor(props) {
    super(); // 没传 props
    console.log(this.props); // undefined！
    // React 内部不会帮你把 props 挂到 this 上
    // 只有 super(props) 才会触发 React.Component 内部的 this.props = props
  }
}

// 正确：super(props)
class Button extends React.Component {
  constructor(props) {
    super(props);
    console.log(this.props); // { label: "Click", onClick: fn }
  }
}
```

**不调用 super(props) 会怎样？**

```text theme={null}
不调用 super():
  → JavaScript 引擎直接报错 ReferenceError
  → 代码根本跑不起来

调用 super() 但不传 props:
  → constructor 内 this.props = undefined
  → constructor 中无法访问 props
  → render() 等其他生命周期中 this.props 仍然正常
     （因为 React 在调用生命周期方法前会重新赋值 this.props）

所以：
  只有在 constructor 中需要访问 this.props 时，才"必须"传 props
  但最佳实践是"永远传 props"，避免踩坑
```

> [!NOTE]
>
> `constructor` 是 Class 的构造方法，用于初始化 state 和绑定 this；`props` 是父组件传入的只读数据；`super(props)` 是调用父类构造函数并让 `this.props` 在 constructor 中可用——不调用 `super()` 会直接报错，不传 `props` 会导致 constructor 中 `this.props` 为 `undefined`。

***

## Q8: setState 什么时候是同步，什么时候是异步？

> [!TIP]
>
> setState 的批量更新就像餐厅的下单系统。在 React 事件回调内（餐厅正常营业时），你连点三道菜（三次 setState），服务员不会每点一道就跑去厨房下单，而是等你点完后统一把菜单送到后厨（批量更新）。但在 setTimeout 等异步上下文中（React 17 的"非营业时间"），你每点一道菜，服务员就立刻跑去厨房下单（同步更新）。React 18 的 Automatic Batching 就像餐厅升级为 24 小时营业——不管什么时候点菜，都是统一送厨房。

### 批量更新的本质

`setState` 的"异步"本质是批量更新（Batching）。React 将多次 `setState` 合并到一个队列中，在当前执行栈结束后的微任务中统一处理，而不是每次 `setState` 都立即更新 DOM。

### setState 的异步场景

在 React 事件回调和生命周期方法中，`setState` 是"异步"的（更准确地说是**批量更新**）。React 会把多次 `setState` 合并成一次渲染，而不是每次 `setState` 都立即更新 DOM。

```javascript theme={null}
class Counter extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    console.log("setState 前:", this.state.count); // 0

    this.setState({ count: this.state.count + 1 });
    console.log("setState 后:", this.state.count); // 0（还没更新！）

    this.setState({ count: this.state.count + 1 });
    console.log("再次 setState 后:", this.state.count); // 0（还是没更新！）

    // 最终只触发一次渲染，count = 1（不是 2！因为两次读到的都是 0）
  };

  render() {
    return <button onClick={this.handleClick}>{this.state.count}</button>;
  }
}
```

为什么最终 `count` 是 1 而不是 2？因为 React 18 之前，`setState({ count: this.state.count + 1 })` 中的 `this.state.count` 在同一个事件回调内始终是**更新前的值**（0）。两次 `setState` 都是 `{ count: 0 + 1 }`，最后一次覆盖前一次，结果是 1。

### setState 的同步场景

在 `setTimeout`、`Promise`、原生 DOM 事件等非 React 管理的上下文中，React 17 及以前的 `setState` 会立即触发渲染，表现为"同步"：

```javascript theme={null}
class Counter extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    // React 事件回调内：批量更新
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count); // 0（异步，还没更新）

    setTimeout(() => {
      // setTimeout 内：React 17 立即更新，React 18 仍然是批量更新
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count); // React 17: 1, React 18: 取决于调度时机
    }, 0);

    // Promise 回调：React 17 立即更新
    Promise.resolve().then(() => {
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count); // React 17: 1
    });
  };
}
```

### React 18 之后的变化：Automatic Batching

React 18 引入了 Automatic Batching（自动批处理），将批处理范围从 React 事件扩展到了**所有上下文**：

```text theme={null}
React 17 及以前                       React 18+
┌──────────────────────┐            ┌──────────────────────┐
│ onClick（React 事件）  │            │ onClick（React 事件）  │
│   setState × 3        │            │   setState × 3        │
│   → 1 次渲染       │            │   → 1 次渲染       │
├──────────────────────┤            ├──────────────────────┤
│ setTimeout / Promise  │            │ setTimeout / Promise  │
│   setState × 3        │            │   setState × 3        │
│   → 3 次渲染       │            │   → 1 次渲染       │
├──────────────────────┤            ├──────────────────────┤
│ 原生 DOM 事件          │            │ 原生 DOM 事件          │
│   setState × 3        │            │   setState × 3        │
│   → 3 次渲染       │            │   → 1 次渲染       │
└──────────────────────┘            └──────────────────────┘
```

```javascript theme={null}
// React 18 的 Automatic Batching 示例
import { createRoot } from "react-dom/client";

function Counter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    // React 17: 在 setTimeout 中会触发 2 次渲染
    // React 18: 自动批处理，只触发 1 次渲染
    setTimeout(() => {
      setCount((c) => c + 1);
      setFlag((f) => !f);
    }, 0);
  }

  return <button onClick={handleClick}>{count}</button>;
}

// React 18 必须用 createRoot 才能启用 Automatic Batching
const root = createRoot(document.getElementById("root"));
root.render(<Counter />);
```

### setState 的批量更新机制

React 的批量更新本质上是将多个 `setState` 调用收集到更新队列中，等当前执行栈结束后，由调度器统一安排一次渲染（React 18 的并发渲染通常通过 `MessageChannel` 以宏任务形式调度）：

```text theme={null}
handleClick 执行
     │
     ├── setState({ a: 1 })  → 放入更新队列
     ├── setState({ b: 2 })  → 放入更新队列
     ├── setState({ c: 3 })  → 放入更新队列
     │
     ▼
handleClick 执行完毕（同步代码跑完）
     │
     ▼
调度回调（宏任务）：React 统一处理队列
     │
     ├── 合并 state: { a: 1, b: 2, c: 3 }
     ├── 执行一次 render()
     └── 更新一次 DOM
```

如果需要在批量更新中立即获取最新 state，可以使用 `setState` 的函数式写法：

```javascript theme={null}
class Counter extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    // 错误：对象式写法，每次读到的 this.state.count 都是旧值
    this.setState({ count: this.state.count + 1 }); // 0 + 1 = 1
    this.setState({ count: this.state.count + 1 }); // 0 + 1 = 1（覆盖）
    this.setState({ count: this.state.count + 1 }); // 0 + 1 = 1（覆盖）
    // 最终 count = 1

    // 正确：函数式写法，参数是上一次的最新 state
    this.setState((prevState) => ({ count: prevState.count + 1 })); // 0 + 1 = 1
    this.setState((prevState) => ({ count: prevState.count + 1 })); // 1 + 1 = 2
    this.setState((prevState) => ({ count: prevState.count + 1 })); // 2 + 1 = 3
    // 最终 count = 3
  };
}
```

**函数式写法 vs 对象式写法：**

| 场景 | 对象式 `setState({})` | 函数式 `setState(fn)` |
| - | - | - |
| 单次调用 | 正常工作 | 正常工作 |
| 同一事件内多次调用 | 后面的覆盖前面的（读到旧值） | 每次拿到最新的 state |
| 依赖上一次 state 时 | 不可靠 | 推荐使用 |

> [!NOTE]
>
> `setState` 在 React 事件和生命周期中是批量更新（看起来像异步），在 setTimeout/Promise/原生事件中 React 17 是同步、React 18 也是批量更新；多个 `setState` 依赖上一次 state 时必须用函数式写法，否则会被覆盖。

***

## Q9: 底层源码——Automatic Batching 是如何实现的？

> [!TIP]
>
> Automatic Batching 的实现就像快递分拣中心的运作模式。不管快递是从哪个渠道来的——网购（onClick）、电话下单（setTimeout）、朋友寄的（Promise）——到了分拣中心（React 的调度系统）后，都会被归类到同一个批次（Lane 集合）。分拣中心用 `MessageChannel` 安排一辆卡车（宏任务），在固定时间统一把这批快递送出去（批量渲染）。Lanes 优先级模型就像快递上的标签——"加急件"（SyncLane）优先配送，"普通件"（DefaultLane）排队等候。

### Automatic Batching 的调度机制

React 18 的自动批处理是怎么做到跨越宏任务的？

在 React 17 及以前，只有 React 自己管理的事件回调（如 `onClick`）内部的 `setState` 才会合并更新。而在 Promise、`setTimeout` 等原生异步任务中，每次 `setState` 都会立即触发一轮完整渲染。React 18 彻底解决了这个问题，详见 [React 源码 packages/react-reconciler/src/ReactFiberWorkLoop.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberWorkLoop.js#L784)。

```javascript theme={null}
// React 18 简化的调度逻辑
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
  // 1. 将更新产生的 Lane（优先级）合并到根节点
  markRootUpdated(root, lane, eventTime);

  // 2. 交由调度器统一安排一次渲染（并发更新通过 MessageChannel 以宏任务调度）
  // 不论触发上下文是 onClick 还是 setTimeout，都会被合并进同一次调度中批量消费
  ensureRootIsScheduled(root, eventTime);
}
```

这段代码做了 2 件事：第一，所有更新不论来源都通过 `markRootUpdated` 合并到根节点的 Lane 集合中；第二，`ensureRootIsScheduled` 判断本次任务优先级，低优先级任务通过 `MessageChannel` 调度一个宏任务来统一消费所有待处理的更新，实现自动批处理（同步优先级的更新仍会同步执行）。

### Lanes 优先级模型

Lanes 优先级模型在源码中长什么样？

详见 [React 源码 packages/react-reconciler/src/ReactFiberLane.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberLane.js)：

```javascript theme={null}
// Lanes 用 32 位二进制位图表示优先级
export const SyncLane = 0b0000000000000000000000000000001;
export const InputContinuousLane = 0b0000000000000000000000000000100;
export const DefaultLane = 0b0000000000000000000000000010000;
export const TransitionLane1 = 0b0000000000000000000000001000000;

// 位运算实现极速优先级判断
// 合并优先级：按位或
mergeLanes(SyncLane, DefaultLane); // 0b10001（17）

// 判断是否包含某优先级：按位与
(lanes & SyncLane) !== 0; // true = 包含同步任务
```

Lanes 模型的优势在于：通过位运算（`&`、`|`），React 可以在 O(1) 时间内判断哪些任务属于同一批次、哪些高优任务插了队，比 ExpirationTime 的数字比较更高效。

***

## Q10: 及格回答和满分回答差在哪？

### 面试评分标准

考察 React 历史和架构演进时，区分"知道版本号"和"理解设计决策"的关键在于：你是否能说出每次架构变革背后的技术瓶颈和设计权衡。

### 问题："说说 React 的历史和架构演进"

**及格回答（60-70分）：**

> "React 是 Facebook 在 2013 年开源的，React 16 引入了 Fiber 架构，React 18 引入了并发模式，React 19 引入了编译器优化。"

问题出在：**知道版本号但不理解驱动力**。

**满分回答（90-100分）：**

> React 的诞生源于 Facebook 2010 年的 UI 维护危机——PHP + XHP 拼接 DOM 导致通知系统代码混乱不堪。Jordan Walke 受函数式编程启发，构建了 FaxJS 原型，核心思想是"UI = f(state)"：状态变化时重新生成虚拟 DOM 树，框架自动 diff 更新。2013 年开源后，虚拟 DOM + JSX 的组合逐渐被业界接受。

> React 的每一次大版本升级都是在解决一个具体的技术瓶颈。React 15 的 Stack Reconciler 用同步递归对比虚拟 DOM，大型应用一次 setState 可能阻塞主线程 100ms 导致掉帧。React 16 的核心突破是用 Fiber 链表替代递归调用栈，实现了时间切片——每执行完一个 Fiber 节点就检查剩余时间，没时间就让出主线程。

> 但 Fiber 只解决了"能中断"的问题，没有解决"该中断什么"。React 18 引入了 Lanes 优先级模型（32 位二进制位图），让调度器可以区分同步任务、连续交互任务和过渡任务的优先级。同时 Automatic Batching 将批处理从 React 事件扩展到所有异步上下文（setTimeout、Promise），彻底解决了 setState 散弹式渲染的问题。

> React 19 的方向是"让框架帮你优化"。React Compiler 通过静态分析自动添加 memoization，开发者不再需要手写 useMemo/useCallback。use() hook 统一了资源消费方式，可以在条件语句中使用。Actions 允许把异步函数直接交给表单或 `useActionState`，由 React 内置维护 pending 与错误状态，减少了手写 loading/error 的样板代码。

### 常见追问

> [accordion] React 18 的并发更新会不会出现 UI 撕裂 (Tearing) ?
>
> **答题要点：**
> 核心在于区分 Render 阶段和 Commit 阶段的中断特性。
>
> **高分回答：**
> "对 React 自己维护的 state 和 props 来说，不会。并发中断仅仅发生在内存中的 Render 阶段（计算对比阶段）。当真正要把差异更新到真实 DOM 的 Commit 阶段时，操作是绝对同步且不可中断的。因此用户看到的 UI 永远是原子化的一致状态——要么是完全的旧状态，要么是完全的新状态。但要注意：如果组件直接读取 React 之外的可变数据源（如全局变量、外部 store），并发渲染期间该数据源可能被修改，从而出现撕裂。这类外部数据应通过 `useSyncExternalStore` 订阅，保证每次渲染读到一致快照。另外不在 Render 阶段写副作用代码，也是并发安全的前提。"

> [accordion] React 为什么要用 Lanes 替代 ExpirationTime?
>
> **答题要点：**
> 从数据结构的表达能力角度解释设计决策。
>
> **高分回答：**
> "ExpirationTime 是一个数字，只能表达线性的优先级高低，无法表达'一组任务'的交叉概念。Lanes 采用 32 位二进制位图（Bitmask），每个 bit 代表一个优先级通道。通过位运算（按位与 `&`、按位或 `|`），React 可以在 O(1) 时间内判断哪些任务属于同一批次、哪些高优任务插了队。例如 `SyncLane` 是第 0 位（`0b1`，值为 1），`TransitionLane1` 是第 6 位（`0b1000000`，值为 64）。在复杂动画和大数据表格同屏时，Lanes 能精准地把动画帧的渲染剥离出来优先投递给浏览器。"

> [accordion] 微前端场景下 React 16 和 React 18 共存, 事件冒泡顺序是什么?
>
> **答题要点：**
> 必须结合 React 17 事件委托节点的变化来分析。
>
> **高分回答：**
> "React 16 将事件代理到 `document`，React 17+ 代理到 Root Node。点击子应用（React 18）时，事件先冒泡到子应用的 Root Node，触发 React 18 的合成事件逻辑；随后继续冒泡到 `document`，触发父应用 React 16 的监听器。如果在 React 18 的 onClick 中调用了原生事件的 `e.stopPropagation()`，事件在原生 DOM 树上停止冒泡，父应用的监听器根本不会被触发。这种行为差异是微前端升级时最常见的坑。"

> [accordion] React Compiler 自动 memoization 会不会取代 useMemo/useCallback?
>
> **答题要点：**
> 对比手动优化和编译器优化的适用范围与局限性。
>
> **高分回答：**
> "React Compiler 的目标确实是让开发者不再需要手写 useMemo/useCallback。它通过静态分析组件代码，自动识别哪些值需要缓存、哪些函数引用需要稳定。但它有明确的边界：第一，编译器只能优化符合 Rules of React 的纯组件，包含外部副作用或可变数据的代码无法自动优化；第二，编译器不理解业务语义——比如一个昂贵的 API 请求，它不知道这个结果应该在什么粒度上复用；第三，编译器已发布稳定版本，但仍需通过 eslint-plugin-react-compiler 排查不兼容代码，存量项目能覆盖多少取决于代码规范程度。短期内 useMemo/useCallback 仍是必要的手动工具，长期来看编译器会接管大部分通用场景。"

***

## 延伸阅读与引用参考

* [React: The Birth of a New Language](https://www.youtube.com/watch?v=8pDqJVdNa44)：Jordan Walke 在 2019 年的演讲，回顾 React 从 FaxJS 到开源的完整历程。<sup>\[1]</sup>
* [Pete Hunt: React - Rethinking Best Practices (JSConf 2013)](https://www.youtube.com/watch?v=x7cQ3mrcKaY)：React 开源时的首次公开演讲，首次提出"UI = f(state)"的声明式范式。<sup>\[2]</sup>
* [React v18.0 Release Notes](https://react.dev/blog/2022/03/29/react-v18.html)：Dan Abramov 撰写的官方发布说明，全面阐述 Concurrent Mode、Automatic Batching 和 Suspense for Data Fetching。<sup>\[3]</sup>
* [React 源码中的 Lanes 模型 PR](https://github.com/facebook/react/pull/18796)：Andrew Clark 提交的 Lanes 设计思路，解释了从 ExpirationTime 到 Bitmask 的演进。<sup>\[4]</sup>
* [Sebastian Markbage: Designing Server Rendering's Future (React Summit 2020)](https://www.youtube.com/watch?v=JEr36pgGotI)：RSC 的设计动机——零 bundle 组件、直接访问后端资源、自动代码分割。<sup>\[5]</sup>
* [React 17 事件委托变更](https://react.dev/blog/2020/08/10/react-v17-rc.html#changes-to-event-delegation)：官方解释微前端多版本共存的事件系统变更。<sup>\[6]</sup>
* [React Compiler 文档](https://react.dev/learn/react-compiler)：官方对 React Compiler 自动优化机制的权威解释。<sup>\[7]</sup>
* [崔健（卡颂）：React 技术揭秘](https://react.iamkasong.com/)：像素级剖析 React Fiber 源码，国内 React 底层原理第一参考。<sup>\[8]</sup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.