React.js 框架
在前端开发的世界里,React 凭借其“声明式、组件化、Learn Once Write Anywhere”的设计哲学,定义了现代前端开发的范式。它的核心思想可以用一个极其优雅的公式概括:UI = f(state)(视图是状态的映射)。无论 React 如何从 Class 时代演进到 Hooks 时代,甚至步入并发渲染模式,其核心始终围绕着单向数据流与不可变性 (Immutability)。
1. 核心基石:JSX 与 单向数据流
1.1 JSX:JavaScript 的语法糖
- 本质:JSX 并不是 HTML,它最终会被 Babel 或 SWC 编译成
React.createElement()(或新的_jsx函数)调用,生成纯粹的 JavaScript 对象(即 Virtual DOM)。 - 优势:赋予了 UI 描述以完整的图灵完备能力。你可以直接在视图层使用
map渲染列表、使用三元运算符进行条件渲染,彻底打破了传统模板引擎的局限。
1.2 单向数据流 (One-way Data Flow)
- React 强制规定数据只能从父组件流向子组件。
- 🚨 核心红线:绝对不要直接修改 State(和 Props)!无论是 Class 组件里的
this.state还是 Hooks 里的state。React 强调不可变性,必须通过setState或dispatch来生成一个全新的引用,React 才能感知到变化并触发重新渲染。
2. 核心范式:Hooks 与 副作用管理
自从 React 16.8 引入 Hooks 以后,函数组件彻底翻身,成为了处理状态和副作用的核心范式。
2.1 State 与衍生数据 (useState & useMemo)
useState/useReducer:用于声明组件的内部状态。当你调用 set 函数时,React 会把组件加入更新队列,并在下一次渲染时返回最新的状态值。useMemo(类似 Vue 的 computed):自带缓存。由于函数组件每次渲染都会从头执行,useMemo可以缓存昂贵的计算结果。如果依赖项数组 (deps) 没有变化,它就会直接返回上一次的缓存值。- 滥用警告:不要给所有变量都套上
useMemo,缓存本身也是有内存和对比开销的!只在进行大规模计算或为了保持引用稳定以配合React.memo时使用。
2.2 Effect 与 生命周期 (useEffect & useEffectEvent)
useEffect-> 与外部系统同步:它是逃生舱。用于处理如网络请求、DOM 订阅、定时器等“副作用”。- 依赖数组 (Deps):这是 Hooks 最容易翻车的地方。如果依赖漏写,会拿到旧数据(闭包陷阱);如果乱写,会导致无限循环。
- 高级进阶 (
useEffectEvent):React 官方正在推进的实验性 Hook。完美解决了“我想在 Effect 里读取最新的状态,但又不想因为这个状态的改变而重新触发 Effect 重新执行”的痛点,将响应式依赖与非响应式逻辑彻底解耦。
3. 核心进阶:Virtual DOM 与 Fiber 架构
为什么 React 渲染庞大应用依然流畅? 答案在于其底层的架构演变。
- Virtual DOM (虚拟 DOM):用轻量级的 JS 对象描述真实 DOM,屏蔽了跨平台的差异。
- Diff 算法 (协调 Reconciliation):采用同层比较的启发式算法。遇到不同类型的节点直接销毁重建;遇到相同节点则对比属性更新。
- React Fiber 架构 (16 之后):可中断的异步渲染。传统的递归 Diff 会阻塞主线程,导致浏览器掉帧卡顿。Fiber 将渲染任务拆分成一个个微小的工作单元,利用浏览器的空闲时间去计算(类似链表的遍历),计算完毕后再统一 Commit 到 DOM 上。
🔑 并发模式 (Concurrent Features): 有了 Fiber 打底,React 18 引入了并发特性。例如 useTransition,它可以将某些状态更新标记为“非紧急(低优先级)”。在处理复杂视图切换或大量数据过滤时,即使后台在疯狂计算,也不会阻塞用户的点击或输入,UI 依然丝滑如顺。
4. 核心架构:组件通信与逻辑复用
4.1 组件通信全家桶
- 父 -> 子:
Props。 - 子 -> 父:通过父组件传递下来的回调函数 (Callback Props) 传递数据。
- 跨层级透传:
Context API。解决多层级“属性钻取” (Prop Drilling) 的痛苦。 - 全局状态:Zustand, Redux, Jotai 等。
4.2 逻辑复用的终极形态:Custom Hooks
- 过去时代的眼泪:早期 React 依靠 Mixins、高阶组件 (HOC) 和 Render Props 来复用逻辑,导致了可怕的“嵌套地狱” (Wrapper Hell) 和 Props 命名冲突。
- 现在的标准解法:自定义 Hook(以
use开头的函数)。只需将相关的 State 和 Effect 抽离到一个纯函数中(如useFetch或useMousePosition),便可实现纯粹的逻辑复用,且与 UI 渲染完美剥离,状态完全独立。
5. 核心周边:前端路由与状态管理
5.1 React Router (前端路由)
- 提供声明式的路由配置,支持嵌套路由和动态路由。
- 在 React Router v6 之后,全面拥抱 Hooks API (
useNavigate,useParams),并引入了基于 Data Router 的 Loader 和 Action 机制,在路由层就解决掉数据预获取的问题。
5.2 状态管理生态 (百花齐放)
与 Vue 官方强绑定不同,React 生态极其自由:
- Redux / RTK (Redux Toolkit):老牌霸主,强调单一数据源和强约束的 Flux 架构。配置繁琐但可预测性极强,适合超大型应用。
- Zustand:极简、现代。没有样板代码,不需要用 Context 包裹整个应用,轻量级首选。
- Jotai / Recoil:原子化 (Atomic) 状态管理。非常适合状态极其分散、需要精准控制细粒度重渲染的场景。
6. 常见问题 (FAQ) 与避坑指南
6.1 为什么我调用了 setCount(count + 1),下一行 console.log(count) 打印的还是旧的值?
- 答:因为 React 的状态更新是“快照”且批处理的。
- 在同一个事件循环(Event Handler)中,React 会把多次
setState收集起来合并,直到事件处理函数执行完毕,才会触发一次重渲染(Batching)。 - 在当前的渲染闭包中,
count的值被锁定在了渲染那一刻的快照。 - 解法:如果下一次状态依赖于上一次状态,请使用函数式更新:
setCount(prev => prev + 1);如果需要在状态更新后执行逻辑,请在useEffect里监听这个状态的变化。
- 在同一个事件循环(Event Handler)中,React 会把多次
6.2 令人抓狂的“闭包陷阱 (Stale Closures)”是怎么回事?
- 答:这是使用 Hooks 时最常见的 Bug。
- 场景:在
useEffect开了一个setInterval打印count,你会发现它永远打印的是初始值(比如 0)。 - 原因:因为
useEffect的依赖数组写错了(通常是写了[]),导致 Effect 里的定时器回调函数永远“困”在了第一次渲染的闭包里,那个闭包里的count永远是 0。 - 解法:1. 正确填写依赖数组
[count],并在 return 中清除旧的定时器;2. 使用useRef保存最新的值,因为 ref 的current属性是可变的,不会受到闭包影响;3. 拥抱useEffectEvent。
- 场景:在
6.3 为什么列表渲染的 key 千万别用 index(索引)?
- 答:会导致极度诡异的组件状态混乱和性能灾难。
key是 React 在 Diff 算法中识别节点的唯一标识。如果使用index,当你在列表顶部插入一条新数据时,所有元素的index都会发生改变。- React 会认为:0号元素被修改了,1号元素被修改了...从而导致原本应该复用的真实 DOM 被全量重新修改。
- 更可怕的是,如果列表项是包含了不受控输入框的子组件,旧组件的内部状态(比如输入框里的字)会错误地挂载到新数据对应的组件上,发生“张冠李戴”。永远使用数据本身的唯一 ID 作为 key!
6.4 useEffect 引起无限循环(死循环)的原因?
- 答:核心在于引用类型的依赖更新。
- 如果
useEffect的依赖项是一个对象或数组,而在 Effect 内部又去触发状态更新导致组件重新渲染。因为每次渲染都会重新创建新的对象或数组(内存地址改变),React 比较依赖项时(Object.is判断)发现它变了,于是再次执行 Effect,从而陷入死循环。 - 解法:1. 尽量依赖基础类型(如对象的
id);2. 把对象/函数的创建用useMemo或useCallback包裹起来;3. 将不需要响应式的变量抽离到组件外部。
- 如果