React 从入门到精通:环境搭建、核心语法、组件库选型与 AI 时代的开发方式
- 约 22 分钟读完 - 全文 8634 字React 从入门到精通
目标读者:会 HTML/CSS/JS 基础,想系统学前端框架的同学;或写过 Vue/jQuery 想转 React 的工程师。 阅读目标:把环境搭起来、把核心语法学会、把坑提前踩掉、知道组件库怎么选,最后聊聊 AI 时代写前端的新姿势。
React 的入门门槛不高,但「会写」和「写对」之间隔着一批经典的坑。这篇按真实学习顺序组织:环境 → 语法 → 心智模型 → 踩坑 → 组件库 → Demo → AI 时代的开发方式。
一、学习路线图
flowchart LR
A["JS 基础<br/>ES6 语法"] --> B["环境搭建<br/>Node + Vite"]
B --> C["核心语法<br/>JSX / 组件 / Props / State"]
C --> D["Hooks<br/>useState / useEffect / 自定义 Hook"]
D --> E["工程化<br/>路由 / 状态管理 / 组件库"]
E --> F["进阶<br/>性能优化 / 服务端渲染"]
F --> G["AI 协同开发"]
二、开发环境搭建(2022 年的正确姿势)
2.1 装 Node:用 nvm,不要官网下载安装包
第一个坑往往出在环境上:老教程让你去 nodejs.org 下安装包,半年后项目要求换个 Node 版本,你就得重装。用 nvm 管理多版本:
# Windows 用 nvm-windows,Mac/Linux 用 nvm
nvm install 16 # 2022 年的 LTS 是 16/18
nvm use 16
node -v # v16.x.x国内网络记得配镜像:npm config set registry https://registry.npmmirror.com。
2.2 创建项目:用 Vite,不要 create-react-app
2022 年的第二个坑:老教程还在教 create-react-app(CRA),它启动慢、打包慢、官方已基本停止维护。用 Vite,秒级启动:
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev # 打开 http://localhost:5173目录结构:
my-app/
├── index.html # 入口 HTML
├── package.json
├── vite.config.js
└── src/
├── main.jsx # 挂载根组件
├── App.jsx # 根组件
└── components/ # 自己的组件放这里2.3 编辑器与插件
VS Code + 两个必装插件:ES7+ React Snippets(输入 rafce 一键生成组件骨架)、ESLint(边写边提示 Hooks 依赖遗漏等问题)。再开 VS Code 的保存自动格式化(Prettier),团队风格从此不打架。
三、核心语法:记住一个公式就够了
3.1 心智模型:UI = f(state)
React 的全部思想一句话:界面是状态的函数。你不直接操作 DOM,只改数据(state),React 负责把界面更新到和数据一致。
flowchart LR
S["state 变化<br/>setState"] --> R["React 重新执行组件函数<br/>得到新的虚拟 DOM"]
R --> D["Diff:对比新旧虚拟 DOM<br/>找出最小差异"]
D --> DOM["只把差异 patch 到真实 DOM"]
这就是为什么 React 代码里永远不要 document.getElementById 去改 DOM——绕过状态直接改界面,两边就对不上了。
3.2 组件与 JSX
组件就是一个返回 JSX 的函数。JSX 是「长得像 HTML 的 JS」,注意三条规则:标签必须闭合、属性用驼峰(className 不是 class)、JS 表达式放 {} 里:
function Welcome({ name }) { // props 是父组件传进来的参数
return <h1 className="title">你好, {name}!</h1>;
}
function App() {
return <Welcome name="React" />; // 组件像标签一样使用
}3.3 state 与事件:让界面动起来
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0); // 声明状态:当前值 + 修改函数
return (
<button onClick={() => setCount(count + 1)}>
点了 {count} 次
</button>
);
}规则:props 是父组件给的(只读),state 是自己的(用 set 函数改)。数据流向永远是父 → 子,这叫单向数据流,是 React 排查问题比双向绑定框架容易的根本原因。
3.4 列表渲染与 key
function TodoList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.text}</li> // key 用稳定 id,不要用 index!
))}
</ul>
);
}3.5 useEffect:处理副作用
数据请求、定时器、订阅这类「和渲染无关的事」放 useEffect:
useEffect(() => {
fetch("/api/todos")
.then((r) => r.json())
.then(setTodos);
}, []); // 依赖数组为空 = 只在组件首次渲染后执行一次四、踩坑故事:六个我真实掉进去过的坑
坑 1:setState 是「异步」的,连续 +1 只加了一次
// 想点一下加 3,结果只加了 1
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);原因:三次 count 读的都是本次渲染的旧值。改法:用函数式更新,每次拿到最新值:
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1); // 这次真的 +3教训:新 state 依赖旧 state 时,永远用函数形式。
坑 2:useEffect 里 setState,死循环刷爆页面
useEffect(() => {
setList([...list, newItem]); // list 变了 → 重新渲染 → effect 又执行 → 死循环
}, [list]);排查套路:页面卡死/请求刷屏,先找「effect 的依赖里有没有它自己改的东西」。解法通常是改用函数式更新把依赖摘掉:setList((l) => [...l, newItem]),依赖数组就能清空。
坑 3:列表 key 用 index,勾选状态全乱了
Todo 列表勾了第 1 项,删掉它之后,第 2 项变成「已勾选」——因为 key 用 index 时,React 按位置复用 DOM,删除一项后后面所有项位置错位,复用错了组件内部状态。key 必须用数据的稳定 id。
坑 4:直接改 state 对象,界面死活不更新
user.name = "新名字"; // 页面没反应!
setUser(user); // 还是没反应——引用没变,React 认为没变化React 靠引用比较判断要不要重渲染。正确姿势是造一个新对象:
setUser({ ...user, name: "新名字" });数组同理:用 filter/map/concat 造新数组,不要 push/splice。
坑 5:定时器里的 stale closure(闭包旧值)
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 永远是 0!
}, 1000);
return () => clearInterval(timer);
}, []);effect 只在首次渲染执行,回调里捕获的 count 永远是首次渲染的旧值。解法:把 count 加进依赖数组,或用 useRef 保存最新值。这类 bug 不报错、只是数据不新鲜,最难查——ESLint 的 exhaustive-deps 规则能提前拦住大半,别随手关掉。
坑 6:在组件里定义组件,输入框每敲一个字就失焦
function Form() {
const Input = () => <input />; // 每次渲染都是全新组件,DOM 被销毁重建
return <Input />;
}组件定义必须放模块顶层。这个坑的典型症状是「input 输入一个字符就丢焦点」,看到这个词直接想到它。
五、底层原理:为什么 React 是这样设计的
前面六个坑,背规则可以避开,但理解原理才能「预判」。这一章讲透四件事:虚拟 DOM 与 Diff、Fiber 架构、渲染两阶段、Hooks 的实现。学完你会发现:第四节所有的坑,都是这四个原理的直接推论。
5.1 虚拟 DOM 与 Diff 算法:用 JS 对象换性能
虚拟 DOM 就是用普通 JS 对象描述界面长什么样({ type: 'div', props: {...}, children: [...] })。每次 state 变化,React 重新生成一棵新的虚拟 DOM 树,和旧的对比(Diff),只把差异更新到真实 DOM——直接操作 DOM 慢,操作 JS 对象快,这就是虚拟 DOM 的收益。
全量对比两棵树的算法复杂度是 O(n³),一万个节点就是十亿次比较,不可用。React 用三条「妥协」策略把它降到 O(n):
flowchart TB
subgraph 策略["Diff 的三条妥协策略"]
T["tree diff:只同层比较<br/>不跨层移动节点"]
C["component diff:组件类型变了<br/>整棵子树直接销毁重建"]
E["element diff:同层列表<br/>靠 key 识别 谁移动/新增/删除"]
end
T --> R["结果:O(n) 完成对比"]
C --> R
E --> R
三条策略各自对应一条实践纪律:
- tree diff → 跨层级移动 DOM 节点代价极大,设计组件树时保持层级稳定;
- component diff → 同一个位置不要渲染「类型忽变」的组件(
<div>和<span>来回切换),类型一变整棵重建; - element diff → 这就是坑 3 的原理:key 是 React 识别「哪个节点是哪个」的唯一依据,用 index 当 key,删除中间项后后面所有 key 全变,React 就把节点张冠李戴了。
5.2 Fiber 架构:把渲染拆成可中断的小任务
React 15 之前,更新是递归遍历组件树,一旦开始就不能停——组件树大了,一帧 16ms 里算不完,页面就掉帧卡顿。React 16 重写架构,引入 Fiber:
- Fiber 是什么:每个组件对应一个 Fiber 节点,渲染工作被拆成一个个以 Fiber 为单位的小任务;
- 怎么连起来:节点之间不用递归,用链表(
child第一个子节点 /sibling下一个兄弟 /return父节点)——用指针代替调用栈,所以随时可以暂停、恢复、放弃; - 双缓存:内存里同时存在两棵树——屏幕上正在显示的
current树,和后台正在构建的workInProgress树。新树在后台慢慢算,算完一次性把指针切过去,用户永远看不到「渲染到一半」的界面。
flowchart LR
subgraph 旧架构["React 15 栈调和"]
A1["递归遍历整棵树<br/>不可中断"] --> A2["大树直接卡顿掉帧"]
end
subgraph 新架构["React 16+ Fiber"]
B1["每个 Fiber 一个小任务<br/>可随时暂停让出主线程"] --> B2["高优先级任务插队<br/>(用户输入 > 列表渲染)"]
B2 --> B3["workInProgress 树算完<br/>一次性切换"]
end
时间切片带来的直接体验:输入框输入(高优先级)可以打断长列表渲染(低优先级),输入再也不卡。useTransition 就是把这个优先级能力开放给开发者——把「不紧急的更新」包进 startTransition,界面保持响应。
5.3 渲染两阶段:render 可中断,commit 不可中断
一次更新分两个阶段,分工严格:
flowchart LR
subgraph R["render 阶段(可中断)"]
R1["调用组件函数<br/>生成新虚拟 DOM"]
R2["Diff,算出变更清单"]
end
subgraph C["commit 阶段(不可中断)"]
C1["把变更一次性<br/>写入真实 DOM"]
C2["执行 useLayoutEffect<br/>再异步执行 useEffect"]
end
R --> C
这一阶段划分解释了两个「为什么」:
- 为什么组件函数里不能有副作用(发请求、改全局变量):render 阶段可中断、可重试、可能被调用多次,副作用会重复执行。所以副作用必须放
useEffect——它在 commit 之后执行,每次提交只跑一次。坑 2 的死循环本质也是没搞清这个边界:effect 里改 state 触发新一轮 render→commit→effect,链条首尾相接就成了循环。 - 为什么 setState 看起来是异步的(坑 1):setState 只是「登记一个更新任务」,组件函数要等 React 调度到 render 阶段才重新执行。你在
setCount之后立刻读count,读到的当然是本次渲染的旧值。
5.4 Hooks 的实现:一条链表 + 调用顺序
Hooks 没有魔法。每个 Fiber 节点上挂着一条链表,组件每次调用一个 Hook(useState/useEffect/…),React 就顺着链表取下一个节点存状态:
flowchart LR
F["Fiber 节点<br/>memoizedState"] --> H1["Hook 节点 1<br/>useState count"]
H1 --> H2["Hook 节点 2<br/>useState text"]
H2 --> H3["Hook 节点 3<br/>useEffect"]
H3 --> N["null"]
关键:状态不是按变量名存的,是按调用顺序存的。第 n 次调用 useState,就拿链表上第 n 个节点。这就是「Hook 不能写在 if/循环里」这条铁律的唯一原因——条件调用会让本次渲染的调用顺序和上次错位,状态全部错位:
// ❌ 条件调用:show 为 false 时少调一次,后面所有 Hook 状态错位
if (show) {
const [a, setA] = useState(0);
}
const [b, setB] = useState(0);用一个 20 行的迷你实现体会一下原理:
// 极简版 useState:一个数组 + 一个游标
const hookStates = [];
let cursor = 0;
function useState(initial) {
const i = cursor++; // 本次调用占第 i 个槽位
hookStates[i] = hookStates[i] ?? initial;
const setState = (v) => {
hookStates[i] = typeof v === "function" ? v(hookStates[i]) : v;
cursor = 0; render(); // 重置游标,重新渲染
};
return [hookStates[i], setState];
}
// 每次 render 前 cursor 归零,Hook 按调用顺序对号入座
真实 React 里这个数组换成 Fiber 上的链表、render 换成调度器触发,思想一模一样。也顺带解释了坑 5(stale closure):effect 回调捕获的是「它创建那次渲染」的作用域,而 Hook 槽位里的状态早已更新——两者不是一份数据,于是读到旧值。
5.5 批量更新:多次 setState 合并成一次渲染
React 会把同一个事件循环里的多次 setState 合并(batching)成一次渲染,避免白白渲染三次。React 18 起连 Promise、setTimeout 里的更新也自动批量。坑 1 的「连续 +1 只加一次」正是批量更新的副产品:三次调用都基于同一个旧值计算,合并后只生效最后一次;函数式更新 setCount(c => c + 1) 之所以能正确累加,是因为每次计算都基于「上一个更新任务的结果」而不是旧快照。
5.6 一张图总结:从 setState 到屏幕更新
flowchart TB
A["setState 登记更新任务"] --> B["Scheduler 调度<br/>按优先级插队/合并"]
B --> C["render 阶段(可中断)<br/>沿 Fiber 链表逐个执行组件函数<br/>Hooks 按调用顺序取状态"]
C --> D["生成 workInProgress 树<br/>Diff 出变更清单"]
D --> E["commit 阶段(不可中断)<br/>一次性写入真实 DOM"]
E --> F["执行 useEffect"]
F -->|"effect 里又 setState"| A
回头看第四节的坑:坑 1 是批量更新、坑 2 是 render/commit 边界、坑 3 是 element diff、坑 4 是引用比较(Diff 的判断依据)、坑 5 是 Hooks 快照机制、坑 6 是 component diff(类型变=重建)。规则会过时,原理不会——这就是先学原理的价值。
六、主流 UI 组件库:选型对比
别从零写按钮和表格,组件库选一个用熟:
| 组件库 | 风格 | 适合场景 | 备注 |
|---|---|---|---|
| Ant Design (antd) | 企业级、组件全 | 中后台管理系统(国内事实标准) | 文档中文友好,生态最厚 |
| Material UI (MUI) | Google Material 风格 | 海外产品、工具类 | 定制性强 |
| Ant Design Mobile | 移动端 | H5/混合 APP | 与 antd 同门 |
| Tailwind + Headless UI | 原子化 CSS | 定制化要求高的产品页 | 没有现成组件,只有样式积木 |
| Chakra UI | 简洁现代 | 快速原型 | 主题系统好用 |
中后台选 antd 基本不会错。三分钟搭出一个像样的页面:
import { Table, Button, Form, Input } from "antd";
function UserPage() {
const columns = [
{ title: "姓名", dataIndex: "name" },
{ title: "邮箱", dataIndex: "email" },
];
return (
<>
<Form layout="inline" onFinish={(v) => console.log(v)}>
<Form.Item name="name" label="姓名"><Input /></Form.Item>
<Button type="primary" htmlType="submit">查询</Button>
</Form>
<Table columns={columns} dataSource={[]} rowKey="id" />
</>
);
}组件库避坑:不要混用两个重型组件库(打包体积爆炸);版本升级先看迁移指南(antd 4→5 改了样式方案);表单校验规则写在 rules 里而不是手写 if-else。
七、完整 Demo:一个 Todo App
把前面的知识拼起来——增删改查、状态提升、组件拆分,都在这 60 行里:
import { useState } from "react";
function App() {
const [todos, setTodos] = useState([]);
const [text, setText] = useState("");
const add = () => {
if (!text.trim()) return;
setTodos((t) => [...t, { id: Date.now(), text, done: false }]); // 新数组!
setText("");
};
const toggle = (id) =>
setTodos((t) => t.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
const remove = (id) => setTodos((t) => t.filter((x) => x.id !== id));
return (
<div style={{ maxWidth: 400, margin: "40px auto" }}>
<h1>Todo</h1>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={add}>添加</button>
<ul>
{todos.map((item) => (
<li key={item.id}>
<span
onClick={() => toggle(item.id)}
style={{ textDecoration: item.done ? "line-through" : "none" }}
>
{item.text}
</span>
<button onClick={() => remove(item.id)}>删除</button>
</li>
))}
</ul>
<p>共 {todos.length} 项,完成 {todos.filter((x) => x.done).length} 项</p>
</div>
);
}
export default App;注意里面处处是前面坑点的正确示范:setTodos 全部函数式更新、key 用 id、改对象用展开运算符造新对象。
八、AI 时代:怎么少写代码完成 APP 开发
写完上面这些,必须承认一件事:这篇博客里 80% 的代码,AI 现在都能直接生成。Todo、表单、表格页这类「模式明确」的代码,人的价值已经不在敲键盘上。我现在的前端开发流程变成了这样:
flowchart LR
A["人:写清需求<br/>页面结构 / 字段 / 交互 / 接口契约"] --> B["AI:生成组件代码<br/>一次给完整页面而非碎片"]
B --> C["人:Review + 跑起来验证<br/>重点看 state 设计和依赖数组"]
C --> D["AI:按反馈迭代<br/>改样式 / 加校验 / 拆组件"]
D --> E["人:集成进项目<br/>接真实接口 / 统一风格"]
几条实战经验:
- 描述越具体,代码越能直接用。「写个用户管理页」和「用 antd Table 写用户管理页,列:姓名/邮箱/状态,顶部一个内联查询表单,分页 pageSize 20,数据从 GET /api/users 拿」得到的代码质量天差地别——这和给实习生派活是一个道理。
- 让 AI 一次生成完整页面,而不是一次次要碎片。碎片代码的集成成本比生成成本高;整页生成后你再删改,快得多。
- Review 的重点变了:语法 AI 很少错,要盯的是架构层面的东西——state 放哪层合不合理、useEffect 依赖有没有坑(本文第四节的坑 AI 也会犯)、组件拆分粒度、key 用没用 index。基本功越扎实,Review 越快越准——AI 淘汰的是只会写不会看的人。
- 工具形态:代码补全(Copilot)管「写着顺」;对话式 IDE(Cursor、Claude Code)管「整个功能」;v0.dev 这类管「从截图/描述直接出页面」。三者是不同粒度,按任务大小选。
- APP 整体路径:需求描述 → AI 出页面原型 → 选定组件库统一风格 → AI 按接口文档接数据 → 人工处理鉴权、错误处理、边界情况。一个中后台 CRUD 类 APP,这个流程能把工期压到原来的三分之一。
九、延伸话题:为什么国内流行 Vue,海外是 React 统治
先校准这个说法:不是非黑即白——国内大厂核心产品用 React 的不少(蚂蚁的 Ant Design 就是 React 生态),海外 Laravel 社区也带起了一批 Vue 用户。但总体格局确实存在:国内中小厂、外包、移动端 H5 里 Vue 占比明显高,海外从创业公司到大厂 React 一家独大(npm 全球周下载量 React 常年是 Vue 的数倍)。原因是多层的:
flowchart TB
subgraph 历史时机["历史时机(2014-2016)"]
H1["Vue 2014 年发布<br/>作者尤雨溪是华人<br/>中文文档从第一天就是一流的"]
H2["恰逢国内移动互联网爆发<br/>海量新人入行<br/>需要低门槛框架"]
H3["AngularJS 在国内已流行<br/>Vue 继承模板+双向绑定心智<br/>平滑承接"]
end
subgraph 生态锁定["生态锁定"]
E1["饿了么 Element UI(2016)<br/>阿里系采用 Vue"]
E2["Element / Ant Design Vue / Vant<br/>中后台组件库成套"]
E3["uni-app 用 Vue 语法写小程序<br/>吃透国内小程序红利"]
end
subgraph 正循环["正循环"]
L1["Vue 门槛低 → 培训机构主教 Vue"]
L2["会 Vue 的人多 → 企业招 Vue 便宜快"]
L3["企业用 Vue → 岗位多 → 新人继续学 Vue"]
end
subgraph 海外对照["海外对照"]
O1["Facebook 背书 + 自家产品验证"]
O2["React Native 吃下跨端"]
O3["Next.js 等元框架强化生态"]
O4["大厂招聘需求锁定人才供给"]
end
H1 & H2 & H3 --> 国内["国内 Vue 流行"]
E1 & E2 & E3 --> 国内
L1 --> L2 --> L3 --> 国内
O1 & O2 & O3 & O4 --> 海外["海外 React 统治"]
拆开看,几个关键变量:
- 文档与语言的近因效应。Vue 的中文文档从发布第一天就是母语级质量,而 React 中文文档长期靠社区翻译、滞后且质量参差。对自学者,这个差异是决定性的——学什么框架,往往就是「哪份文档看得懂」决定的。
- 生态位被谁先占住。2016 年饿了么开源 Element UI、阿里系大量采用 Vue,中后台组件库(Element/Ant Design Vue/Vant)成套出现,「Vue + Element」成了国内中后台的标准答案。海外对应位置被 React 的 Ant Design(蚂蚁国际版反哺)、MUI、Next.js 占住。
- 培训机构的放大器作用。Vue 模板语法接近 HTML、双向绑定写表单快、SFC 单文件组件直观——对从后端/jQuery 转前端的人极其友好,培训周期短、就业反馈快,于是机构主教 Vue,供给端被锁定。
- 移动端的分野。海外跨端 = React Native,进一步巩固 React;国内跨端的真正战场是小程序,而 uni-app 这类「用 Vue 语法写小程序」的框架吃掉了这块红利,又反过来强化了 Vue 心智。
- React 的门槛是真实的。JSX、函数式思维、不可变数据、Hooks 的心智负担(本文四、五章的坑就是证据),对英语文档和工程化基础都有要求——这在海外是标配,在国内快速扩张期的团队里是成本。
对学习者的启示:两个框架的核心思想(组件化、状态驱动 UI、虚拟 DOM)完全相通,Vue3 的组合式 API 和 React Hooks 已经趋同。学会一个,另一个一周能上手——别把宝押在框架上,押在原理上。而且 AI 时代框架差异进一步被抹平:AI 两个都写得很好,你需要的只是读懂它写的是什么的判断力。
十、思考与感悟
1. React 的本质不是语法,是「UI = f(state)」这个心智模型。 Hooks 语法三天能学会,但「什么该进 state、state 放哪层、如何从 state 推导 UI」要想清楚需要几个月。判断一个人 React 水平,不要问语法,问他「这个状态你会放在哪个组件」。
2. 坑大多是「引用」和「时机」两类。 本文六个坑归个类:坑 1/3/4 是没理解引用比较(key 复用、state 不可变),坑 2/5/6 是没理解渲染时机(effect 何时跑、闭包捕获哪次渲染的值)。把「每次渲染都是一次全新函数调用」这句话想透,大半坑自动消失。
3. 组件库是杠杆,不是拐杖。 antd 让中后台效率翻倍,但只会在组件里塞业务逻辑的人,换个库就废了。组件库之上是交互设计,之下是 React 基本功,两头都得懂一点。
4. AI 时代,「读代码」比「写代码」更值钱。 生成不再稀缺,判断力才稀缺。能一眼看出 AI 生成的代码里 useEffect 依赖少了、state 设计反了的人,会把 AI 用得越来越顺手;只会复制粘贴的人,会被 AI 生成的坑埋掉。学基础不是为了手写,是为了 Review。
十一、学习路线建议
- 第一周:搭好 Vite 环境,跟着官方教程(react.dev 的互动教程质量很高)过完 JSX/组件/state。
- 第二周:手写 Todo App,故意踩一遍第四节的坑(比如把 key 换成 index 看看怎么乱),踩过才算学会。
- 第三周:加路由(React Router)和组件库(antd),做一个有列表页+表单页的两页应用。
- 第四周:接一个真实后端 API,处理 loading/错误/空数据三种状态——真实项目和 Demo 的差距全在这里。
- 进阶:自定义 Hook 抽逻辑、Context/Zustand 管全局状态、性能优化(memo/useMemo)、Next.js 服务端渲染。
参考资料:
- React 官方文档(react.dev)——新版文档质量极高,教程可交互
- Vite 官方文档
- Ant Design 官方文档
- React Router 文档