Skip to content

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 强调不可变性,必须通过 setStatedispatch 来生成一个全新的引用,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 渲染庞大应用依然流畅? 答案在于其底层的架构演变。

  1. Virtual DOM (虚拟 DOM):用轻量级的 JS 对象描述真实 DOM,屏蔽了跨平台的差异。
  2. Diff 算法 (协调 Reconciliation):采用同层比较的启发式算法。遇到不同类型的节点直接销毁重建;遇到相同节点则对比属性更新。
  3. 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 抽离到一个纯函数中(如 useFetchuseMousePosition),便可实现纯粹的逻辑复用,且与 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 里监听这个状态的变化。

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. 把对象/函数的创建用 useMemouseCallback 包裹起来;3. 将不需要响应式的变量抽离到组件外部。