主题
第 11 章 · React 入门
一句话开篇:React 把"操作 DOM"这种麻烦事抽象成了"描述界面应该是什么样",你只管声明,剩下的交给框架。
0. 生活类比(先建立直觉)
类比一:饭店点菜单 vs 自己跑后厨
把"渲染页面"想象成"做一顿饭":
- 原生 JS 操作 DOM = 你自己跑进后厨,亲手洗菜、切菜、炒菜、装盘、端上桌。每加一个菜都要重新跑一趟。
- React = 你只需要在菜单上勾选"我要番茄炒蛋、麻婆豆腐、米饭",把菜单(JSX)交给服务员(React),后厨(虚拟 DOM + Diff)会自动决定哪些菜要重做、哪些菜不用动。
你的工作从 "怎么做" 变成了 "我想要什么"——这就是声明式 UI。
类比二:乐高积木
React 把界面拆成一个个乐高积木(组件):
- 一个按钮是一块积木
- 一张卡片是几块积木拼起来的
- 一个完整页面 = 大积木 = 小积木的组合
每个积木都有:
- 形状(JSX 模板)
- 颜色/大小(props,外部传入)
- 是否亮起灯光(state,自己内部管理)
1. React 是什么
- 定位:Meta(Facebook)开源的 UI 库(严格说不是完整框架)
- 诞生:2013 年,Facebook 为了解决 Newsfeed 频繁刷新带来的 DOM 操作复杂度问题
- 核心理念:
- 声明式 UI:你描述"界面长什么样",不是"如何一步步操作 DOM"
- 组件化:一切皆组件,可复用、可组合
- 虚拟 DOM + Diff:内存中先算差异,再批量更新真实 DOM
- 单向数据流:数据从父组件流向子组件,状态变化触发重新渲染
React 只关心"视图层",路由(React Router)、状态管理(Redux / Zustand)、数据请求(React Query)都需要配合社区方案。
2. 为什么需要 React(对比原生 JS)
场景:实现一个计数器
原生 JS 写法:
html
<div id="root">
<button id="btn">点击了 0 次</button>
</div>
<script>
let count = 0;
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
count++;
btn.textContent = `点击了 ${count} 次`; // 手动同步 DOM
});
</script>每次状态变化,都要手动找到 DOM 节点 + 手动修改文本。如果界面有 50 处依赖 count,你得记住 50 处都更新。
React 写法:
jsx
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>点击了 {count} 次</button>;
}只需声明"按钮上写着 count",状态一变 React 自动重渲染。你写的是"结果",不是"步骤"。
3. JSX 语法
JSX = JavaScript + XML,让你在 JS 里写"看起来像 HTML"的代码。
jsx
const name = 'React';
const element = <h1 className="title">Hello, {name}!</h1>;JSX 不是 HTML,要注意:
| HTML 写法 | JSX 写法 | 原因 |
|---|---|---|
class="x" | className="x" | class 是 JS 关键字 |
for="id" | htmlFor="id" | for 是 JS 关键字 |
onclick="..." | onClick={...} | 驼峰命名 |
style="color:red" | style={{color:'red'}} | 是对象,不是字符串 |
tabindex | tabIndex | 驼峰 |
<br> | <br /> | 必须自闭合 |
JSX 的本质:语法糖
JSX 最终会被 Babel 编译成 React.createElement 调用:
jsx
const el = <h1 className="title">Hello</h1>;
// ↓ 编译后
const el = React.createElement('h1', { className: 'title' }, 'Hello');React.createElement 返回一个普通 JS 对象(虚拟 DOM 节点),形如:
js
{
type: 'h1',
props: { className: 'title', children: 'Hello' },
key: null,
ref: null
}JSX 中插入表达式
jsx
const user = { name: '小明', age: 18 };
const list = ['React', 'Vue', 'Svelte'];
<div>
<p>姓名:{user.name}</p>
<p>年龄:{user.age + 1}</p>
<p>是否成年:{user.age >= 18 ? '是' : '否'}</p>
<ul>
{list.map(item => <li key={item}>{item}</li>)}
</ul>
</div>⚠️ 大括号里只能写表达式,不能写
if/for等语句。要用三元运算符或&&、map。
4. 组件(Component)
函数组件(推荐 ✅)
jsx
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// 使用
<Welcome name="React" />类组件(旧写法,了解即可)
jsx
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}函数组件 vs 类组件
| 维度 | 函数组件 | 类组件 |
|---|---|---|
| 写法 | 普通函数,返回 JSX | 继承 React.Component,写 render() |
| 状态 | useState 等 Hooks | this.state + this.setState |
| 副作用 | useEffect | componentDidMount 等生命周期 |
| this | 没有 this 困扰 | 频繁需要 bind(this) |
| 代码量 | 少 | 多 |
| 性能 | 略好(无实例化开销) | 略差 |
| 现状 | 官方推荐,社区主流 | 历史遗留,不推荐新写 |
自 React 16.8(2019)引入 Hooks 后,函数组件已全面取代类组件。本笔记只学函数组件。
5. Props(属性)
Props 是父组件传给子组件的数据,像函数的参数。
jsx
function UserCard({ name, age, avatar }) {
return (
<div className="card">
<img src={avatar} />
<h3>{name}</h3>
<p>{age} 岁</p>
</div>
);
}
// 使用
<UserCard name="小明" age={18} avatar="/me.jpg" />Props 的核心特性
- 只读(Immutable):子组件不能修改 props,只能读
- 单向流动:从父 → 子,子组件想"通知"父组件,要靠回调函数
- 可以是任何类型:字符串、数字、对象、数组、函数、JSX
children(特殊 prop)
children 表示组件标签内部的内容:
jsx
function Card({ children }) {
return <div className="card">{children}</div>;
}
<Card>
<h3>标题</h3>
<p>内容</p>
</Card>6. State(状态)
State 是组件自己管理的、会变化的数据。State 一变,组件自动重新渲染。
jsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 初始值 0
return (
<div>
<p>当前:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}useState 的关键点
- 数组解构:
[当前值, 修改函数] - 不可变更新:不能直接
count++,必须调用setCount - 异步批处理:连续多次
setCount会被合并
jsx
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// 结果只 +1,因为三次读到的 count 都是同一个旧值正确写法(基于上一个状态):
jsx
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// 结果 +3 ✅7. 事件处理
jsx
function Button() {
const handleClick = (e) => {
e.preventDefault();
console.log('点击了', e.target);
};
return <button onClick={handleClick}>Click</button>;
}注意点
- 事件名是驼峰:
onClick、onChange、onMouseEnter - 事件处理器是函数引用,不是字符串:
onClick={handleClick},不是onClick="handleClick()" - 要传参,用箭头函数包裹:
onClick={() => handleClick(id)} - React 事件是合成事件(SyntheticEvent),包装了原生事件以便跨浏览器一致
8. 条件渲染
jsx
function Greeting({ isLogin }) {
// 写法 1:if 提前 return
if (!isLogin) return <p>请登录</p>;
return <p>欢迎回来</p>;
// 写法 2:三元
return isLogin ? <p>欢迎回来</p> : <p>请登录</p>;
// 写法 3:&& 短路
return isLogin && <p>欢迎回来</p>;
}⚠️
count && <p>{count}</p>:如果count === 0,会渲染出0!要写成count > 0 && ...或Boolean(count) && ...。
9. 列表渲染
jsx
const items = ['苹果', '香蕉', '橙子'];
<ul>
{items.map(item => (
<li key={item}>{item}</li>
))}
</ul>为什么要 key?
React 用 key 来识别列表中的元素,判断哪些新增、哪些删除、哪些复用。
| key 用法 | 说明 |
|---|---|
key={item.id} ✅ | 用稳定唯一 id |
key={index} ⚠️ | 列表静态时可以;动态增删会出 bug |
| 不写 key ❌ | 控制台会警告,性能差 |
10. Hooks 基础
Hooks 是 React 16.8 引入的"在函数组件里用状态和副作用"的机制。函数组件 + Hooks = 现代 React。
useState — 状态
见第 6 节。
useEffect — 副作用
副作用 = 组件渲染之外的事,比如:发请求、订阅事件、操作 DOM、setTimeout。
jsx
useEffect(() => {
// 副作用代码
console.log('组件挂载或更新了');
return () => {
// 清理函数(卸载时或下次副作用前执行)
console.log('清理');
};
}, [依赖数组]); // 依赖变化才会重新执行依赖数组的三种写法
| 写法 | 时机 |
|---|---|
useEffect(fn) | 每次渲染后都执行(很少用) |
useEffect(fn, []) | 仅挂载时执行一次(相当于 componentDidMount) |
useEffect(fn, [a, b]) | a 或 b 变化时执行 |
经典例子:定时器
jsx
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(timer); // 必须清理!否则内存泄漏
}, []);经典例子:发请求
jsx
useEffect(() => {
fetch(`/api/user/${id}`)
.then(r => r.json())
.then(data => setUser(data));
}, [id]); // id 变化重新请求useRef — 引用
useRef 返回一个可变的引用对象,.current 属性可以存任何东西,修改它不会触发重渲染。
用途 1:访问 DOM
jsx
function AutoFocus() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 直接操作 DOM
}, []);
return <input ref={inputRef} />;
}用途 2:保存"不需要触发渲染"的值
jsx
const timerRef = useRef(null);
const start = () => {
timerRef.current = setInterval(...);
};
const stop = () => {
clearInterval(timerRef.current);
};useState vs useRef
| 维度 | useState | useRef |
|---|---|---|
| 修改触发渲染 | ✅ 是 | ❌ 否 |
| 用途 | 需要响应式更新 UI 的数据 | DOM 引用 / 不参与渲染的值 |
| 读写方式 | [val, setVal] = useState() | ref.current |
11. React 渲染流程(ASCII 图)
┌────────────────────────────────────────────────────────┐
│ 用户操作 │
│ (点击 / 输入 / ...) │
└─────────────────────────┬──────────────────────────────┘
│ 触发事件处理函数
▼
┌──────────────────┐
│ setState(...) │
│ setCount(c+1) │
└─────────┬─────────┘
│
▼
┌─────────────────────────┐
│ 调度更新 (Scheduler) │
│ 生成新的虚拟 DOM 树 │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Diff 算法(Reconciler)│
│ 比较新旧虚拟 DOM │
│ 找出最小变化集 │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Renderer 提交到真实 DOM │
│ (react-dom) │
└────────────┬────────────┘
│
▼
🖥 浏览器更新12. 函数组件生命周期对照(Hooks 实现)
| 类组件生命周期 | Hooks 等价写法 |
|---|---|
componentDidMount | useEffect(() => {...}, []) |
componentDidUpdate | useEffect(() => {...}, [deps]) |
componentWillUnmount | useEffect(() => { return () => {...} }, []) |
getDerivedStateFromProps | 直接根据 props 计算(或 useMemo) |
shouldComponentUpdate | React.memo + useMemo / useCallback |
13. 实战案例
详见 examples/ 目录:
counter.jsx— 计数器(useState 基础)todo-list.jsx— Todo 列表(增删改查、列表渲染)fetch-data.jsx— 数据请求(useEffect + 异步)
demo/index.html — 综合 Demo:天气查询小应用(useState + useEffect + 真实 API)
14. ⚠️ 易踩的坑
直接修改 state
jsx// ❌ state.count++; list.push(item); // ✅ setCount(count + 1); setList([...list, item]);React 用
Object.is比较引用,直接修改原对象不会触发渲染。在循环 / 条件 / 嵌套函数里调用 Hooks
jsx// ❌ 违反 Hooks 规则 if (cond) { const [x, setX] = useState(0); }必须在组件顶层调用,每次渲染顺序一致。
useEffect 依赖数组漏写
jsxuseEffect(() => { fetch(`/api/${id}`); // 用了 id }, []); // ❌ 漏写 id,永远只请求第一次的 idsetState 后立即读 state
jsxsetCount(count + 1); console.log(count); // ❌ 还是旧值!setState 异步,新值要在下次渲染时才生效。要用副作用读:
useEffect(() => console.log(count), [count])。列表 key 用 index 动态增删时会引发"组件状态错位"(如 input 内容串到别的项)。
JSX 必须有唯一根节点
jsxreturn ( <h1>A</h1> <h2>B</h2> // ❌ ); // ✅ 用 Fragment return <><h1>A</h1><h2>B</h2></>;事件处理写成调用而非引用
jsx// ❌ 渲染时立即执行 <button onClick={handleClick()}>... // ✅ <button onClick={handleClick}>... <button onClick={() => handleClick(id)}>...忘记清理副作用 订阅、定时器、WebSocket 都要在
useEffect返回函数里清理,否则内存泄漏。
15. 一句话总结
React 让你只关心"界面长什么样"(声明式),用组件化搭积木,用 Hooks 管状态和副作用,剩下的脏活累活交给虚拟 DOM 和 Diff 算法。
16. 延伸阅读
- 官方文档(中文):https://zh-hans.react.dev/
- React 18 新特性:并发渲染、自动批处理、Suspense for Data
- 下一章:第 12 章 React 进阶(自定义 Hooks / 性能优化 / Fiber)