1. 什么是 React
React 是 Meta(原 Facebook)开源的 UI 库——注意是"库"不是"框架"。它只负责视图层(View),不内置路由、状态管理等能力,需要搭配生态中的其他库(React Router、Zustand 等)来构建完整应用。
组件化思想
React 的核心思想是组件化:把 UI 拆分为独立、可复用的小块,每个组件管理自己的状态和渲染逻辑,组件之间通过 Props 通信。就像乐高积木,复杂界面由简单组件组合而成。
声明式 vs 命令式:传统 DOM 操作是命令式的——"找到这个元素,改变它的文本"。React 是声明式的——"当 count 是 5 时,UI 应该长这样"。你描述应该是什么,React 负责让 DOM 变成那样。
2. Virtual DOM 原理
直接操作真实 DOM 是昂贵的——浏览器需要重新计算布局(reflow)和重绘(repaint)。React 引入 Virtual DOM 来优化这个过程。
工作原理
- React 在内存中维护一棵轻量的 JS 对象树,映射真实 DOM 结构
- 当状态变化时,React 生成一棵新的 Virtual DOM 树
- Diff 算法对比新旧两棵树,找出差异(最小变更集)
- 只把差异部分更新到真实 DOM(批量更新)
key 的重要性
React 的 Diff 算法在处理列表时,用 key 来识别每个列表项的"身份"。有了 key,React 能精准判断哪个元素被增删改,避免不必要的重渲染。
// ❌ 没有 key —— React 无法判断哪个元素是哪个
{items.map(item => <li>{item.name}</li>)}
// ✅ 有稳定唯一的 key
{items.map(item => <li key={item.id}>{item.name}</li>)}
3. 创建第一个 React 项目
推荐使用 Vite 创建 React + TypeScript 项目。Vite 是新一代前端构建工具,开发服务器启动极快(利用浏览器原生 ESM),热更新几乎即时。
创建项目
# 创建项目(react-ts 模板自带 TypeScript 配置)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev # 启动开发服务器,默认 http://localhost:5173
项目结构说明
my-app/
├── public/ # 静态资源(不经 Vite 处理,直接复制到 dist)
├── src/
│ ├── main.tsx # 入口文件 —— 将 App 挂载到 #root
│ ├── App.tsx # 根组件
│ ├── App.css # 根组件样式
│ ├── index.css # 全局样式
│ └── vite-env.d.ts # Vite 的类型声明
├── index.html # HTML 模板,Vite 从这里开始构建
├── vite.config.ts # Vite 配置
├── tsconfig.json # TypeScript 配置
└── package.json
main.tsx — 应用入口
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
// 找到 index.html 中的 <div id="root">,将 React 应用挂载到此
createRoot(document.getElementById('root')!).render(
<StrictMode> // 开发模式下额外检查(双调用 render,只在 dev 环境)
<App />
</StrictMode>,
)
4. JSX 规则
JSX(JavaScript XML)是 React 的模板语法,让你在 JS/TS 中写类似 HTML 的代码。但 JSX 不是 HTML,有很多不同之处需要注意。
JSX vs HTML 的主要差异
| HTML | JSX | 原因 |
|---|---|---|
class="btn" | className="btn" | class 是 JS 保留字 |
for="input1" | htmlFor="input1" | for 是 JS 保留字 |
onclick="fn()" | onClick={fn} | 驼峰命名,传函数引用 |
style="color:red" | style={{ color: 'red' }} | style 是 JS 对象 |
<br> | <br /> | JSX 要求自闭合 |
| 可以多个根元素 | 必须有一个根元素 | JSX 转 JS 函数调用 |
Fragment 避免多余 DOM 节点
// ❌ 多个根元素 —— 报错
return (
<h1>Title</h1>
<p>Content</p>
);
// ✅ 用 Fragment 包裹(不生成额外 DOM 节点)
return (
<>
<h1>Title</h1>
<p>Content</p>
</>
);
JSX 表达式 {}
const name = 'Alice';
const isAdmin = true;
const items = ['Apple', 'Banana'];
return (
<div>
{/* {} 中放 JS 表达式(不能放语句)*/}
<p>Hello, {name}!</p>
<p>Score: {90 + 10}</p>
{/* 条件渲染 */}
{isAdmin && <span>Admin</span>}
{isAdmin ? <span>Admin</span> : <span>User</span>}
{/* 列表渲染 */}
<ul>
{items.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</div>
);
5. 函数组件
React 16.8 之前,有状态的组件必须用类(Class Component)来写。Hooks 的引入让函数组件也能有状态,类组件几乎已被废弃。现代 React 全用函数组件。
// 最简单的函数组件 —— 接收 props,返回 JSX
function Hello({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>;
}
// 箭头函数写法(两种都常见)
const Hello = ({ name }: { name: string }) => (
<h1>Hello, {name}!</h1>
);
// 使用(大写开头,与 HTML 标签区分)
<Hello name="World" />
React.FC 的争议:早期常见 const Comp: React.FC<Props> = (props) => ...,但 React.FC 在 React 18 中已移除了隐式的 children: ReactNode,且类型推断有些奇怪。现代推荐直接标注参数类型:function Comp(props: Props)。
6. Props 详解
Props(Properties)是组件的输入参数。父组件通过 Props 向子组件传递数据,就像函数的参数一样。
Props 是只读的
这是 React 的核心约束:子组件不能修改 Props。Props 是单向数据流的体现——数据只从父流向子,子组件通过回调函数(也是 Props)通知父组件更改状态。
定义 Props 接口
interface UserCardProps {
name: string;
age: number;
role: 'admin' | 'editor' | 'viewer';
avatar?: string; // 可选,没有则显示默认头像
onFollow?: () => void; // 可选回调
children?: React.ReactNode; // 接收子内容
}
function UserCard({
name,
age,
role,
avatar = '/default-avatar.png', // 参数默认值(代替 defaultProps)
onFollow,
children
}: UserCardProps) {
return (
<div className="card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{age} · {role}</p>
{onFollow && <button onClick={onFollow}>Follow</button>}
{children} {/* 渲染子内容 */}
</div>
);
}
// 使用
<UserCard
name="Alice"
age={25}
role="admin"
onFollow={() => console.log('followed!')}
>
<p>Additional content here</p>
</UserCard>
children 类型
interface ContainerProps {
children: React.ReactNode; // 最宽泛:JSX、string、number、null...
}
interface SingleChildProps {
children: React.ReactElement; // 必须是单个 JSX 元素
}
interface RenderPropProps {
children: (data: User) => React.ReactNode; // 函数 children
}
7. 组件组合
React 推崇组合(Composition)而非继承。通过 children 和 Props 回调,父组件可以灵活地定制子组件的行为。
条件渲染
function UserPanel({ user, isLoading, error }: PanelProps) {
// 提前返回(Early Return)—— 处理不同状态
if (isLoading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
if (!user) return null; // 不渲染任何东西
return (
<div>
{/* && 短路:只有 user.isAdmin 为 true 才渲染 */}
{user.isAdmin && <span className="badge">Admin</span>}
{/* 三元运算符:两个分支 */}
<p>{user.isActive ? '在线' : '离线'}</p>
</div>
);
}
陷阱:{count && <Comp />} —— 当 count 为 0 时,会在页面上渲染数字 0!因为 0 是假值但不是 null/undefined,React 会渲染它。
正确写法:{count > 0 && <Comp />} 或 {!!count && <Comp />}。
8. 列表渲染
React 使用 JavaScript 的 .map() 来渲染列表,每个列表项必须有唯一的 key prop。
key 为什么不能用 index
用数组下标作为 key 会导致问题:当列表被重排序或中间插入/删除元素时,index 会变化,React 认为"第 2 个元素变了",从而重新渲染了本来没有变化的组件,还可能导致组件状态混乱(比如表单输入内容跑到了错误的行)。
interface Todo {
id: number;
text: string;
done: boolean;
}
function TodoList({ todos }: { todos: Todo[] }) {
return (
<ul>
{todos.map(todo => (
{/* ✅ 用稳定的业务 id,而不是数组 index */}
<li key={todo.id}>
<span
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
>
{todo.text}
</span>
</li>
))}
</ul>
);
}
9. 事件处理
React 使用合成事件(SyntheticEvent)对浏览器原生事件进行跨浏览器封装,提供统一的接口。合成事件遵循 W3C 规范,让你不用担心浏览器兼容性。
常用事件处理示例
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// input 的 onChange 事件
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};
// form 的 onSubmit 事件
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // 阻止默认行为(页面刷新)
console.log({ email, password });
};
// 按键事件
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') handleSubmit(e as any);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={handleEmailChange}
onKeyDown={handleKeyDown}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)} {/* 内联写法 */}
/>
<button type="submit">登录</button>
</form>
);
}
事件处理的 TypeScript 类型参考
| 事件 | TS 类型 |
|---|---|
| input / textarea onChange | React.ChangeEvent<HTMLInputElement> |
| select onChange | React.ChangeEvent<HTMLSelectElement> |
| button onClick | React.MouseEvent<HTMLButtonElement> |
| form onSubmit | React.FormEvent<HTMLFormElement> |
| 键盘事件 onKeyDown | React.KeyboardEvent<HTMLInputElement> |
| 鼠标悬浮 onMouseEnter | React.MouseEvent<HTMLDivElement> |
本章小结:React 的核心是"声明式 UI + 组件化"。掌握 JSX 规则(className、驼峰事件、必须有根元素),理解 Props 是只读的单向数据流,学会列表渲染和事件处理,就具备了 React 开发的基础。下一章深入 Hooks。