React 技术演进史:从 jQuery 到 React 19
理解每次技术变革的动机,才能真正掌握 React 的设计哲学
演进时间线
2013 - React 诞生 ─── 虚拟 DOM、单向数据流
│
2015 - React 0.14 ─── 函数组件、拆分 react-dom
│
2016 - React 15 ─── Stack Reconciler、合成事件优化
│
2017 - React 16 ─── Fiber 架构 🔥 (重写核心)
│
2019 - React 16.8 ─── Hooks 🔥 (编程范式转变)
│
2020 - React 17 ─── 渐进升级、事件委托改变
│
2022 - React 18 ─── Concurrent Features 🔥
│
2024 - React 19 ─── Server Components、Actions、Compiler 🔥🔥
│
2024-2025 - React 19.x ─── Activity、useEffectEvent1. 前 React 时代:jQuery 的困境 (2006-2013)
1.1 jQuery 统治时期
javascript
// jQuery 时代的典型代码
$('#button').click(function() {
var name = $('#input').val();
$('#display').text('Hello, ' + name);
$('#display').addClass('highlight');
if (name.length > 10) {
$('#warning').show();
} else {
$('#warning').hide();
}
});1.2 暴露的问题
| 问题 | 表现 | 后果 |
|---|---|---|
| 命令式操作 | 手动操作 DOM 的每一步 | 代码冗长、易出错 |
| 状态分散 | 状态存在 DOM 中 | 难以追踪、同步困难 |
| 无组件化 | 代码复用靠复制粘贴 | 维护噩梦 |
| 数据流混乱 | 任意位置修改任意 DOM | 调试困难 |
1.3 大型应用的崩溃
Facebook 2011 年的 Chat 功能:
- 多个地方显示未读消息数
- 任何一处更新都可能遗漏其他
- Bug: 打开聊天后未读数不同步
- 原因: 命令式更新无法保证一致性核心矛盾: 命令式编程无法 Scale
2. React 诞生:声明式革命 (2013)
2.1 核心洞察
Facebook 工程师 Jordan Walke 的关键思考:
"如果 UI 是状态的函数,那么 UI = f(state)"
javascript
// 命令式 (jQuery)
$('#count').text(newCount); // 告诉浏览器"怎么做"
// 声明式 (React)
<div>{count}</div> // 告诉 React"要什么"2.2 虚拟 DOM 的诞生
动机:直接操作 DOM 太慢,但声明式需要全量更新
解决方案:在 JS 层做 Diff,最小化 DOM 操作
State A ──→ Virtual DOM A ─┐
├──→ Diff ──→ Patches ──→ Real DOM
State B ──→ Virtual DOM B ─┘2.3 单向数据流
动机:双向绑定在复杂应用中造成数据流向不可预测
┌─────────────────────────────────────────────────┐
│ 双向绑定的问题 (Angular 1.x) │
├─────────────────────────────────────────────────┤
│ Model ◄──────► View ◄──────► Model │
│ 数据流向不明,调试困难 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 单向数据流 (React) │
├─────────────────────────────────────────────────┤
│ State ──→ View ──→ Action ──→ State │
│ 数据流向清晰,可预测 │
└─────────────────────────────────────────────────┘2.4 解决的问题
| 问题 | React 的解决方案 |
|---|---|
| 命令式复杂度 | 声明式 UI = f(state) |
| DOM 操作性能 | 虚拟 DOM + Diff |
| 数据流混乱 | 单向数据流 |
| 代码复用 | 组件化 |
3. Fiber 架构:可中断的革命 (2017, React 16)
3.1 Stack Reconciler 的问题
┌─────────────────────────────────────────────────┐
│ Stack Reconciler (React 15) │
├─────────────────────────────────────────────────┤
│ 渲染大组件树时: │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ 递归调用栈 (同步执行) │ │
│ │ render() → render() → render() → ... │ │
│ │ 不可中断! 100ms+ 卡顿 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 用户点击按钮 → 无响应 → 体验差 │
└─────────────────────────────────────────────────┘3.2 动机:让渲染可中断
核心问题:JS 是单线程,长任务会阻塞用户交互
javascript
// 问题场景
function BigList({ items }) {
return items.map(item => <ComplexItem key={item.id} {...item} />);
// 10000 个 item,渲染需要 500ms
// 这 500ms 用户无法输入、点击、滚动
}3.3 Fiber 的解决方案
关键创新:把递归改成循环,把调用栈改成链表
┌─────────────────────────────────────────────────┐
│ Fiber Reconciler (React 16+) │
├─────────────────────────────────────────────────┤
│ 时间切片 (Time Slicing): │
│ │
│ ┌────┐┌────┐┌────┐┌────┐┌────┐ │
│ │工作 ││让出 ││工作 ││让出 ││工作 │ ... │
│ │ 5ms││ ││ 5ms││ ││ 5ms│ │
│ └────┘└────┘└────┘└────┘└────┘ │
│ ↑ │
│ 每帧预留时间给浏览器,保持响应 │
└─────────────────────────────────────────────────┘3.4 为什么用链表?
javascript
// 递归版本 - 信息在调用栈中,无法保存
function traverse(node) {
process(node);
node.children.forEach(traverse); // 中断后丢失位置
}
// 链表版本 - 信息在 Fiber 节点中,可以保存
let current = root;
while (current && !shouldYield()) {
process(current);
current = getNext(current); // 中断后可以继续
}3.5 解决的问题
| 问题 | Fiber 的解决方案 |
|---|---|
| 长任务阻塞 | 时间切片,可中断 |
| 无优先级 | Lane 模型,优先级调度 |
| 无法恢复 | 链表结构,保存进度 |
4. Hooks:函数式编程的胜利 (2019, React 16.8)
4.1 Class 组件的问题
javascript
// 问题 1: 逻辑难以复用
class UserList extends Component {
componentDidMount() {
this.fetchUsers();
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
// 相关逻辑分散在不同生命周期中
}
// 问题 2: HOC 嵌套地狱
export default withRouter(
withTheme(
withAuth(
withAnalytics(UserList)
)
)
);
// <WithRouter><WithTheme><WithAuth><WithAnalytics><UserList/></...>4.2 动机
| Class 组件问题 | 表现 |
|---|---|
| 逻辑复用难 | HOC、Render Props 嵌套地狱 |
| 生命周期分散 | 相关逻辑分散在 didMount/willUnmount |
| this 困扰 | 绑定 this、箭头函数、bind |
| 难以优化 | Class 难以 tree-shaking |
4.3 Hooks 的解决方案
javascript
// 逻辑复用: 自定义 Hook
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handler = () => setSize({
width: window.innerWidth,
height: window.innerHeight
});
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []); // 相关逻辑集中在一起!
return size;
}
// 使用: 组合而非嵌套
function UserList() {
const size = useWindowSize();
const theme = useTheme();
const auth = useAuth();
// 扁平的、可组合的
}4.4 设计哲学转变
Class 组件时代:
继承 → 生命周期 → 面向对象
Hooks 时代:
组合 → 副作用隔离 → 函数式编程4.5 解决的问题
| 问题 | Hooks 的解决方案 |
|---|---|
| 逻辑复用 | 自定义 Hooks,组合模式 |
| 生命周期分散 | useEffect 集中相关逻辑 |
| this 绑定 | 函数组件,无 this |
| 代码体积 | 更好的 tree-shaking |
5. Concurrent Features:用户体验优先 (2022, React 18)
5.1 问题:CPU 密集型更新
javascript
// 搜索场景
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleChange = (e) => {
setQuery(e.target.value); // 需要立即响应
setResults(search(e.target.value)); // 可能很慢
};
// 问题: 搜索结果计算阻塞了输入框响应
}5.2 动机:区分紧急与非紧急更新
| 更新类型 | 示例 | 期望 |
|---|---|---|
| 紧急 | 输入、点击、按键 | 立即响应 |
| 非紧急 | 搜索结果、列表渲染 | 可以延迟 |
5.3 解决方案:Concurrent Rendering
javascript
function Search() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setQuery(e.target.value); // 紧急更新:同步
startTransition(() => {
setResults(search(e.target.value)); // 非紧急:可中断
});
};
return (
<>
<input value={query} />
{isPending ? <Spinner /> : <Results />}
</>
);
}5.4 Automatic Batching
javascript
// React 17: setTimeout 内每次 setState 都触发渲染
setTimeout(() => {
setCount(c => c + 1); // 渲染
setFlag(f => !f); // 渲染
}, 0);
// React 18: 自动批处理,只渲染一次
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 只有一次渲染!
}, 0);5.5 解决的问题
| 问题 | Concurrent 的解决方案 |
|---|---|
| 所有更新同优先级 | Lane 模型区分优先级 |
| 紧急更新被阻塞 | useTransition 降级非紧急更新 |
| 手动批处理 | Automatic Batching |
| Loading 状态管理 | Suspense + SuspenseList |
6. Server Components:全栈 React (2024, React 19)
6.1 传统 CSR 的问题
┌─────────────────────────────────────────────────┐
│ 传统 CSR 流程 │
├─────────────────────────────────────────────────┤
│ 1. 下载 HTML (空壳) │
│ 2. 下载 JS Bundle (可能很大) │
│ 3. 执行 JS │
│ 4. 发起数据请求 │
│ 5. 渲染 UI │
│ │
│ 问题: │
│ - Bundle 越来越大 (组件库、状态库、工具库) │
│ - 瀑布流请求 (JS → API → 渲染) │
│ - 首屏白屏时间长 │
└─────────────────────────────────────────────────┘6.2 动机:减少客户端 JS
| 传统问题 | 数据 |
|---|---|
| 平均 JS Bundle | 300-500KB (gzip) |
| 依赖库占比 | 60-70% |
| 首屏时间 | 3-5s (慢网络) |
6.3 Server Components 的解决方案
┌─────────────────────────────────────────────────┐
│ Server Components 架构 │
├─────────────────────────────────────────────────┤
│ │
│ Server Component (不发送到客户端) │
│ ├── 直接访问数据库 │
│ ├── 零 Bundle Size │
│ └── 渲染成 HTML + Flight 数据 │
│ │
│ Client Component ('use client') │
│ ├── 需要交互的部分 │
│ ├── 发送到客户端 │
│ └── 可以使用 useState/useEffect │
│ │
└─────────────────────────────────────────────────┘jsx
// Server Component - 不发送 JS
async function BlogPost({ id }) {
const post = await db.query(`SELECT * FROM posts WHERE id = ${id}`);
// 直接访问数据库!
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton postId={id} /> {/* Client Component */}
</article>
);
}
// Client Component - 发送 JS
'use client';
function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(true)}>Like</button>;
}6.4 Server Actions:简化数据变更
jsx
// 传统方式: 需要 API 路由
// pages/api/create-post.js
// fetch('/api/create-post', { method: 'POST', body: ... })
// Server Actions: 直接在组件中定义
async function createPost(formData) {
'use server';
await db.insert('posts', {
title: formData.get('title'),
content: formData.get('content')
});
revalidatePath('/posts');
}
function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" />
<textarea name="content" />
<button type="submit">Create</button>
</form>
);
}6.5 解决的问题
| 问题 | RSC 的解决方案 |
|---|---|
| Bundle 过大 | Server Component 零 Bundle |
| API 层模板代码 | Server Actions 直接变更 |
| 数据获取瀑布 | 服务端获取,Streaming 传输 |
| 首屏性能 | 服务端渲染,增量传输 |
7. React Compiler:告别手动优化 (2024-2025)
7.1 手动优化的心智负担
javascript
// 开发者每天的灵魂拷问
function Component({ data, onClick }) {
// 这个需要 useMemo 吗?
const processed = processData(data);
// 这个需要 useCallback 吗?
const handleClick = () => {
onClick(processed);
};
// 子组件需要 memo 吗?
return <Child data={processed} onClick={handleClick} />;
}
// 过度优化: 到处都是 useMemo/useCallback
// 遗漏优化: 性能问题难以定位
// 错误优化: 依赖数组写错7.2 动机:自动化优化
| 问题 | 表现 |
|---|---|
| 心智负担 | 需要时刻考虑是否需要优化 |
| 容易出错 | 依赖数组遗漏导致 Bug |
| 过度优化 | 不必要的 memo 增加复杂度 |
| 难以教学 | 新手不知道何时优化 |
7.3 React Compiler 的解决方案
javascript
// 编译前 (你写的代码)
function Component({ data, onClick }) {
const processed = processData(data);
const handleClick = () => onClick(processed);
return <Child data={processed} onClick={handleClick} />;
}
// 编译后 (React Compiler 自动生成)
function Component({ data, onClick }) {
const $ = useMemoCache(3);
let processed;
if ($[0] !== data) {
processed = processData(data);
$[0] = data;
$[1] = processed;
} else {
processed = $[1];
}
let handleClick;
if ($[2] !== onClick || $[1] !== processed) {
handleClick = () => onClick(processed);
$[2] = onClick;
$[3] = handleClick;
} else {
handleClick = $[3];
}
return <Child data={processed} onClick={handleClick} />;
}7.4 解决的问题
| 问题 | Compiler 的解决方案 |
|---|---|
| 手动优化负担 | 自动分析依赖,自动记忆化 |
| 依赖数组错误 | 编译时静态分析,不会出错 |
| 过度/遗漏优化 | 精确优化需要的部分 |
| 性能一致性 | 所有组件自动优化 |
8. 演进规律总结
8.1 每次变革的共同点
┌─────────────────────────────────────────────────┐
│ React 演进的核心驱动力 │
├─────────────────────────────────────────────────┤
│ │
│ 1. 降低开发者心智负担 │
│ jQuery → React: 不用手动操作 DOM │
│ Class → Hooks: 不用管理 this │
│ 手动 memo → Compiler: 不用考虑优化 │
│ │
│ 2. 提升用户体验 │
│ Stack → Fiber: 不卡顿 │
│ Sync → Concurrent: 响应优先 │
│ CSR → RSC: 更快首屏 │
│ │
│ 3. 拥抱函数式编程 │
│ Class → Function: 函数组件 │
│ Mutation → Immutable: 不可变数据 │
│ 命令式 → 声明式: 描述结果而非过程 │
│ │
└─────────────────────────────────────────────────┘8.2 技术演进对应的问题
| 版本 | 解决的核心问题 | 引入的新概念 |
|---|---|---|
| React 诞生 | DOM 操作复杂、状态管理混乱 | 虚拟 DOM、组件化、单向数据流 |
| React 16 | 长任务阻塞、无法中断 | Fiber、时间切片、优先级 |
| React 16.8 | 逻辑复用难、Class 臃肿 | Hooks、函数组件 |
| React 18 | 所有更新同优先级 | Concurrent、useTransition |
| React 19 | Bundle 大、API 层繁琐 | RSC、Server Actions |
| Compiler | 手动优化心智负担 | 自动记忆化 |
8.3 未来方向
当前趋势:
├── 全栈 React (RSC + Server Actions)
├── 自动优化 (React Compiler)
├── 更好的 DX (开发者体验)
└── 跨平台统一 (React Native 新架构)
可能的演进:
├── Signals? (更细粒度响应式)
├── 更智能的编译优化
├── 更紧密的服务端集成
└── AI 辅助开发?9. 面试高频问题
Q1: 为什么 React 要从 Stack 架构改为 Fiber?
答:Stack Reconciler 使用递归遍历,一旦开始无法中断。当组件树很大时,会长时间占用主线程,导致用户交互卡顿。Fiber 架构将递归改为可中断的循环,通过时间切片在每帧预留时间给浏览器,保持界面响应。
Q2: Hooks 解决了 Class 组件的什么问题?
答:
- 逻辑复用:HOC/Render Props 嵌套地狱 → 自定义 Hooks 组合
- 关注点分离:生命周期分散逻辑 → useEffect 集中相关逻辑
- this 绑定:Class 的 this 困扰 → 函数组件无 this
- 代码体积:Class 难以优化 → 函数更易 tree-shaking
Q3: React 18 的 Concurrent 特性解决了什么问题?
答:解决了所有更新同等优先级的问题。通过 useTransition 可以将非紧急更新标记为低优先级,让用户输入等紧急更新优先响应,提升用户体验。
Q4: Server Components 的核心价值是什么?
答:
- 减少 Bundle:Server Component 不发送 JS 到客户端
- 简化数据获取:直接在组件中访问数据库,无需 API 层
- 提升首屏性能:服务端渲染 + Streaming 传输
Q5: React Compiler 会改变什么?
答:开发者不再需要手动使用 useMemo、useCallback、memo 进行优化。编译器会静态分析代码,自动在需要的地方插入记忆化逻辑,消除了手动优化的心智负担和出错可能。
Q6: React 的演进体现了什么设计哲学?
答:
- 声明式优于命令式:描述"要什么"而非"怎么做"
- 组合优于继承:Hooks 组合代替 Class 继承
- 用户体验优先:Fiber、Concurrent 都是为了更好的用户体验
- 降低开发者负担:每次演进都在减少需要手动处理的事情