React 为什么 10 年了还有生命力?
React 已经走过了十余年。这期间前端世界也发生了变化,框架换了一茬又一茬,但 React 的核心理念几乎没怎么动摇,甚至在换 Fiber 架构之后,生命力愈发旺盛了。基于这个困惑,有了这篇文章。
一、困惑:React 为什么偏偏不做"框架"?
把时间拨回 2013 年。那时候前端世界正在被 AngularJS 统治——路由给我们配好,HTTP 客户端内置,依赖注入开箱即用,表单验证一条龙。那是一个"框架"的黄金时代,开发者打开终端敲一行命令,一个完整的项目骨架就搭好了。
React 却走了另一条路。
它只给我们一件事:把状态映射成 UI 的能力。路由?自己挑。状态管理?自己决定。HTTP 请求?爱用啥用啥。React 甚至连 DOM 都不碰——渲染交给 react-dom,Native 交给 react-native。
这就让人很困惑了。
困惑一:React 为什么主动放弃"全栈框架"的诱人定位,甘愿做一个"不完整"的库?图什么?
困惑二:状态不可变这件事,用起来总觉得别扭。
this.state.count++明明那么自然,React 为什么非要我们每次都创建一个新对象?
困惑三:
UI = f(state)这行公式到处都能看到,但它到底意味着什么?为什么 React 的 API 长成今天这个样子,而不是别的样子?
困惑四:十年间,Angular 从 1.0 重写到了 17,Vue 从选项式 API 演进到组合式 API,为什么 React 的核心——组件、state、props、单向数据流——几乎没有本质变化?这种稳定性是从哪来的?
这些困惑好像都指向同一个答案:React 的每一个设计决策,都是为"长期适应性"做的深思熟虑的考虑。不是"懒得做框架",而是故意不做框架。这种"减法"背后,是一套经过反复权衡的判断吧(个人思考,也不一定对)。
二、解法:四个塑造 React 命运的抉择
2.1 抉择一:Library,不是 Framework
打开 React 的 package.json,第一行就写得很清楚:
“React is a JavaScript library for building user interfaces.”
不是 framework,不是 platform,不是 suite——是 library。在刚学 React 的时候,我克隆源码下来看到这句话的时候,第一反应是为什么要强调这个?有啥区别?不都说的 React 框架?难道是作者写错了?然后转头就去看源码了,不过那个时候也没看懂,模模糊糊就过去了
对比一下 React 和 Angular
| React(库) | Angular(框架) | |
|---|---|---|
| 控制权 | 我们控制应用结构,React 只管 UI 怎么画到屏幕上 | Angular 控制应用生命周期,我们往它挖好的坑里填代码 |
| 路由 | react-router,社区维护,随时可换 | @angular/router,官方内置,换不掉 |
| HTTP | axios、fetch、React Query,任君挑选 | HttpClient,内置的,不用也得用 |
| 状态管理 | Redux、Zustand、Jotai、Recoil……百花齐放 | NgRx、Akita,官方推荐的路径就几条 |
| 升级代价 | 低,换 renderer 或 reconciler 就行 | 高,框架整体升级,一升全升 |
React 把控制权交还给了开发者。 它只解决一个问题——如何把声明式的组件描述高效地同步到屏幕上。剩下的,让生态来决定。
这种选择的直接后果是:React 可以活得比任何生态趋势都长。Flux 过时了,Redux 顶上来;Redux 太重了,Zustand 冒出来;REST 不够用了,React Query 横空出世。React 本身不需要为这些事操心,它只管自己的那一亩三分地。
换句话说,React 的 “不完整” 恰恰是其长寿的秘诀。
2.2 抉择二:UI = f(state)
React 最核心的 API 设计原则,藏在一条大家都知道,并且看起来像是数学作业的公式里:
UI = f(state)
但这行公式很简洁,也挺好看。它决定了 React 全部 API 的形态——从组件怎么写,到状态怎么传,到更新怎么触发,一切的一切,都能从这条公式里推导出来。
| 数学概念 | React 里的对应物 |
|---|---|
函数定义 f |
组件函数,或者 class 的 render 方法 |
输入参数 state |
props + useState / useReducer |
输出值 UI |
JSX 表达式,本质上是一个 ReactElement |
| 纯函数约束(无副作用) | React.memo、PureComponent、不可变性要求 |
函数组合 f ∘ g |
组件嵌套:<Parent><Child /></Parent> |
这条公式带来了几个深远的影响:
第一,组件必须是"可调用"的。 函数组件成为首选,因为函数是最自然的数学映射单位。一个组件就是一次函数调用,输入是状态,输出是 UI 描述。
第二,输入决定输出。 给定相同的 props 和 state,组件必须渲染出相同的 UI。这听起来像是废话,但它实际上禁止了 render 阶段中的副作用——我们不能在 render 里发 HTTP 请求,不能操作 DOM,不能读本地存储。因为这些事会让"相同输入"不再保证"相同输出"。
第三,组合优于继承。 函数组合(f(g(x)))比类继承更自然。React 的组件嵌套就是函数组合的语法糖,所以 React 从未真正依赖继承——extends Component 只是历史包袱,extends 本身在 React 的设计里没有位置。
2.3 抉择三:单向数据流
React 的数据流动规则简单到近乎粗暴:数据向下流,回调向上传。
父组件(拥有 state)
↓ props
子组件(只读)
↓ callback
父组件(更新 state)
子组件不能直接改父组件的状态。它只能通过父组件预留给它的回调函数,"请求"父组件去改。父组件收到请求,创建新的 state,React 检测到变化,重新渲染。
这个设计消灭了一个折磨前端开发者多年的噩梦:“到底是谁改了这份数据?”
在双向绑定的世界里,任何一个组件都可以在某个不经意的角落修改共享状态。调试的时候,我们得像侦探一样,在十几个文件之间来回跳转,断点打了又删, console.log 写了又写,最后发现是一个三级子组件在某个生命周期钩子里偷偷改了全局变量。
React 的单向流强制了一个清晰的契约:谁拥有 state,谁才能改它。子组件没有修改权,只有"请求修改权"。这听起来像是限制了自由,但实际上,这种限制让程序的行为变得可以推理——看到一个 bug,顺着数据流的方向往上追,总能找到源头。
2.4 抉择四:不可变性约束
React 不允许我们直接修改 state。每一次变更,都必须创建一个新的对象:
// 错的。React 不会检测到变化,因为引用没变。
this.state.count = 1;
this.setState({ count: this.state.count });
// 对的。创建一个新对象,React 看到新引用,触发重新渲染。
this.setState({ count: 1 });
// 或者用函数形式,更安全
this.setState(prev => ({ count: prev.count + 1 }));
这个约束在刚上手的时候确实让人不舒服,甚至有的时候需求写烦的时候会觉得这个设计很蠢。它要求我们改变写代码的习惯——不能 push 数组,得用 concat 或展开运算符;不能 obj.key = value,得用展开运算符复制一份。ES6 之前这简直是噩梦,ES6 有了展开运算符之后才好一些。
但 React 坚持这个约束,是为了两个核心目标:
第一,引用相等性检测(===)。 React 通过比较前后 state 的引用(oldState === newState)来快速判断是否需要重新渲染。如果允许直接修改 state,这个优化就完全失效了——因为引用始终相等,React 永远认为"没有变化"。
第二,时间旅行调试(Time-Travel Debugging)。 如果每个 state 都是不可变快照,我们可以把整个应用的状态历史保存为一个数组,想回退到哪个点就回退到哪个点。Redux DevTools 里那个滑动条之所以能用,就是因为 Redux 的 state 是不可变的。每一个状态都是一个完整的快照,前后互不干扰。
三、源码:当设计思考变成代码
React 的设计意图不是写在文档里的漂亮话,而是深深地烙印在源码的每一行里。让我们打开 facebook/react 仓库,看看这些思考是怎么变成代码的。
3.1 packages/shared/ReactElementType.js —— UI = f(state) 的类型证明
React 的 UI 输出单元叫 ReactElement。看看它的类型定义:
// https://github.com/facebook/react/blob/main/packages/shared/ReactElementType.js
export type ReactElement = {
$$typeof: any, // Symbol 标记,防止 XSS 攻击
type: any, // 组件函数,或字符串标签如 'div'
key: any, // Diff 算法用的标识
ref: any, // DOM 引用
props: any, // 输入参数——函数调用的"实参"
_owner: any, // 创建该元素的组件(仅 DEV 调试用)
_store: { validated: 0 | 1 | 2, ... },
_debugInfo: null | ReactDebugInfo,
_debugStack: Error,
_debugTask: null | ConsoleTask,
};
注意这个结构——ReactElement 是一个纯粹的数据结构。它描述 UI “应该长什么样”,但本身不做任何事情。没有方法,没有副作用,就是一张"说明书"。
关键在那个 type 字段。它可以是字符串('div'、'span'),也可以是函数(组件函数)。当 type 是函数时,React 会调用这个函数,传入 props,得到一个新的 ReactElement。然后递归这个过程,直到 type 变成字符串,生成真实 DOM。
ReactElement(type=FunctionComponent, props={state})
→ 调用 FunctionComponent(props)
→ 返回 ReactElement(type='div', props={children: ...})
→ 递归协调,直到 type 为字符串
→ 生成真实 DOM
这就是 UI = f(state) 在源码中的实现。ReactElement 是整个递归过程的"中间表示",它的不可变性保证了这个过程的纯粹性。
3.2 packages/shared/ReactSymbols.js —— 用 Symbol 筑起安全墙
React 使用 Symbol 来标记不同的元素类型:
// https://github.com/facebook/react/blob/main/packages/shared/ReactSymbols.js
export const REACT_LEGACY_ELEMENT_TYPE: symbol = Symbol.for('react.element');
export const REACT_ELEMENT_TYPE: symbol = Symbol.for('react.transitional.element');
export const REACT_PORTAL_TYPE: symbol = Symbol.for('react.portal');
export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment');
export const REACT_STRICT_MODE_TYPE: symbol = Symbol.for('react.strict_mode');
export const REACT_PROFILER_TYPE: symbol = Symbol.for('react.profiler');
export const REACT_CONSUMER_TYPE: symbol = Symbol.for('react.consumer');
export const REACT_CONTEXT_TYPE: symbol = Symbol.for('react.context');
export const REACT_FORWARD_REF_TYPE: symbol = Symbol.for('react.forward_ref');
export const REACT_SUSPENSE_TYPE: symbol = Symbol.for('react.suspense');
export const REACT_MEMO_TYPE: symbol = Symbol.for('react.memo');
export const REACT_LAZY_TYPE: symbol = Symbol.for('react.lazy');
每个 Symbol 是一个不可伪造的类型标记。$$typeof: REACT_ELEMENT_TYPE 这个字段确保了:一个对象只有是 React 自己创建的,才是合法的 ReactElement。
为什么要这样?为了防止 XSS 攻击。假设服务器被入侵,返回了这样的 JSON:
{ "type": "script", "props": { "src": "evil.com/steal.js" } }
如果前端直接把这份数据当成 ReactElement 渲染,恶意脚本就会被执行。但 Symbol 是 JSON 无法序列化的——攻击者没法伪造 $$typeof: Symbol.for('react.element'),因为 JSON 里写不了 Symbol。React 在渲染前检查 $$typeof,不匹配的直接拒绝。
这体现了防御性编程的设计思路:即使不信任输入数据,系统也能自保。
3.3 packages/react/src/ReactBaseClasses.js —— 极简主义的类组件
React 的类组件基类 Component 精简得令人惊讶:
// https://github.com/facebook/react/blob/main/packages/react/src/ReactBaseClasses.js
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject; // ref 容器,复用的空对象
this.updater = updater || ReactNoopUpdateQueue; // updater 由外部 renderer 注入
}
Component.prototype.isReactComponent = {}; // 标记:这是一个 React 组件
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
逐行看看这里面的设计取舍。
isReactComponent = {}:鸭子类型的典范。React 不检查 instanceof Component,只检查对象上有没有 isReactComponent 属性。这比我们想象的更灵活——任何类,只要带上这个标记,React 都认它是组件。没有强耦合,没有继承绑架。
updater 由外部注入:setState 本身不做事,只是把请求推进队列。真正执行更新的是 react-dom 或 react-native 注入的 updater。这再次体现了库的定位——React 核心不绑死任何渲染逻辑,它只是一个协调层。
源码注释里的那句话:
“You should treat
this.stateas immutable.”
这不是温馨提示,是契约。React 团队在源码文档里就明确告诉我们:state 是不可变的。违反这个契约,代码不会马上崩溃,但会在某个线上事故里让我们背故障指标。
3.4 packages/react/src/jsx/ReactJSXElement.js —— JSX 怎么变成纯数据
我们写的 JSX,经过 Babel 或 TypeScript 编译后,会变成对 jsx() 函数的调用。看看这个函数怎么创建 ReactElement:
// https://github.com/facebook/react/blob/main/packages/react/src/jsx/ReactJSXElement.js
import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols';
function jsx(type, config, maybeKey) {
// 提取 key、ref,合并默认 props……
return ReactElement(
type,
key,
ref,
undefined, // source(生产环境省略)
undefined, // self(生产环境省略)
props,
);
}
function ReactElement(type, key, ref, source, self, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE, // Symbol 标记,防 XSS
type: type, // 组件函数或 DOM 标签
key: key,
ref: ref,
props: props, // 纯数据,没有方法
_owner: __DEV__ ? ReactSharedInternals.A?.getOwner?.() : null,
};
if (__DEV__) {
element._store = { validated: 0 };
element._debugStack = new Error();
}
return element;
}
几个值得品味的细节。
$$typeof: REACT_ELEMENT_TYPE:每个 JSX 元素在编译后都带有一个 Symbol 标记。这意味着攻击者无法通过 JSON 伪造 ReactElement——Symbol 无法被 JSON.stringify,这是 React 的安全基石之一。
props 是纯对象:ReactElement 的 props 不含任何方法,只是一个传递数据的容器。这强制了数据驱动的编程模型——组件之间通过数据通信,而不是通过方法调用。
不用 new,用工厂函数:ReactElement 是普通函数,不是构造函数。React 用函数式风格创建对象,避开了 new 的隐式行为和原型链开销。这是函数式思维在源码细节里的渗透。
3.5 packages/react/src/ReactHooks.js —— 给函数组件装上"状态引擎"
Hooks 是 React 16.8 引入的特性,它让函数组件拥有了状态能力。但 Hooks 的实现方式有点意外——它们不是"挂载"在组件上的,而是通过一个全局 dispatcher 在渲染时临时注入:
// https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js
export function useState(initialState) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
export function useReducer(reducer, initialArg, init) {
const dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}
export function useEffect(create, deps) {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
resolveDispatcher() 是理解 Hooks 的关键。每次渲染时,React 会先把当前组件对应的 dispatcher 设为全局可用,然后执行组件函数。组件函数里的 useState、useEffect 调用,实际上访问的是这个全局 dispatcher。渲染结束后,dispatcher 被清空。
这就是为什么有"Hooks 规则"——只能在最顶层调用 Hooks,不能在循环或条件里调用。因为 Hooks 依赖于调用的顺序来匹配状态,打破了顺序,匹配就乱了。
Hooks 的设计体现了 React 对组合式 API 的坚持:每个 Hook 是一个独立原语(useState、useEffect、useContext),我们可以自由组合它们,而不是被框架强制的生命周期方法所约束。想加状态?加一行 useState。想发请求?加一行 useEffect。没有多余的包袱。
3.6 源码映射
| 设计意图 | 源码文件 | 关键符号/函数 |
|---|---|---|
| UI = f(state) | packages/shared/ReactElementType.js |
ReactElement 类型,type 字段 |
| 类型安全(防 XSS) | packages/shared/ReactSymbols.js |
REACT_ELEMENT_TYPE Symbol |
| 库的独立性 | packages/react/src/ReactBaseClasses.js |
updater 注入,isReactComponent |
| JSX → 纯数据 | packages/react/src/jsx/ReactJSXElement.js |
jsx() 函数,ReactElement() 工厂 |
| 函数式状态 | packages/react/src/ReactHooks.js |
useState, resolveDispatcher |
| 组件纯粹性 | packages/react/src/ReactMemo.js |
React.memo() 高阶组件 |
四、启发:从源码里长出来的工程智慧
4.1 技术选型,先看底层思考
React 团队做每一个技术决策时,都会问一个问题:“这个决策在十年后还会是正确的吗?”
- 库而非框架——框架的全家桶会过时,但 UI 协调的需求不会。React 只做后者。
- 函数式而非面向对象——数学函数比类继承更稳定,组合比继承更灵活。函数式编程的基础保证了它的长期有效性。
- 不可变性而非响应式追踪——不可变数据的语义更简单,调试更容易,而且天然支持并发。响应式追踪(Proxy)虽然方便,但它的"魔法"在复杂场景下会反噬。
当我们为一个新项目做技术选型时,不该只比较"哪个框架功能更多"。功能是多变的,今天有的明天可能就没有了。更该问的是:
“这项技术做出的核心承诺是什么?这个承诺在五年后还会有效吗?”
React 的核心承诺是 UI = f(state)。这个承诺来自数学,数学不会过期。
4.2 用"约束"换取"自由"
React 有一个贯穿始终的工程智慧:主动引入"有益的约束",用短期的便利换取长期的可控。
- 不可变性约束 → 换来了时间旅行调试、引用相等性优化、可预测渲染
- 单向数据流约束 → 换来了状态变更的可追踪性
- 纯函数约束 → 换来了可测试性、可缓存性、可并行性
每一条约束在刚开始的时候都像是一种"限制"——不能这么写,不能那么改。但时间一久,当我们的项目从几千行膨胀到几十万行,当团队成员从 2 个人增长到 20 个人,这些约束就变成了护城河。它们让程序的行为可以被推理,让 bug 的源头可以被追踪,让新人上手时不至于迷失在数据的迷宫里。
在自己的项目里,我们也可以主动引入这类约束:
- 强制 API 的不可变契约——版本化、 immutable infrastructure
- 强制数据流向——事件溯源、CQRS
- 强制无副作用的纯函数核心——领域逻辑与副作用彻底分离
约束不是枷锁,是为了让系统活得更久而做的设计。
4.3 控制反转的生态智慧
React 不做路由,不做 HTTP,不做表单验证。但它培养了世界上最大的前端生态系统。
这是一种平台思维——不拥有全部,但定义游戏规则。React 定义了组件模型、Hook 约定、调度协议,然后让生态在之上自由生长。react-router、React Query、Zustand、Next.js、Remix……这些不是 React 官方的产品,但它们共同构成了比任何单一框架都更丰富的工具图景。
当设计平台或基础设施时,值得问自己:
“我们应该拥有这个功能,还是定义一个让社区拥有它的协议?”
有时候,最好的设计不是做更多,而是划定边界,然后退后一步。
更多推荐

所有评论(0)