Skip to content

第 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 操作复杂度问题
  • 核心理念
    1. 声明式 UI:你描述"界面长什么样",不是"如何一步步操作 DOM"
    2. 组件化:一切皆组件,可复用、可组合
    3. 虚拟 DOM + Diff:内存中先算差异,再批量更新真实 DOM
    4. 单向数据流:数据从父组件流向子组件,状态变化触发重新渲染

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'}}是对象,不是字符串
tabindextabIndex驼峰
<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 等 Hooksthis.state + this.setState
副作用useEffectcomponentDidMount 等生命周期
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 的核心特性

  1. 只读(Immutable):子组件不能修改 props,只能读
  2. 单向流动:从父 → 子,子组件想"通知"父组件,要靠回调函数
  3. 可以是任何类型:字符串、数字、对象、数组、函数、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>;
}

注意点

  • 事件名是驼峰onClickonChangeonMouseEnter
  • 事件处理器是函数引用,不是字符串: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

维度useStateuseRef
修改触发渲染✅ 是❌ 否
用途需要响应式更新 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 等价写法
componentDidMountuseEffect(() => {...}, [])
componentDidUpdateuseEffect(() => {...}, [deps])
componentWillUnmountuseEffect(() => { return () => {...} }, [])
getDerivedStateFromProps直接根据 props 计算(或 useMemo
shouldComponentUpdateReact.memo + useMemo / useCallback

13. 实战案例

详见 examples/ 目录:

  • counter.jsx — 计数器(useState 基础)
  • todo-list.jsx — Todo 列表(增删改查、列表渲染)
  • fetch-data.jsx — 数据请求(useEffect + 异步)

demo/index.html — 综合 Demo:天气查询小应用(useState + useEffect + 真实 API)


14. ⚠️ 易踩的坑

  1. 直接修改 state

    jsx
    // ❌
    state.count++;
    list.push(item);
    // ✅
    setCount(count + 1);
    setList([...list, item]);

    React 用 Object.is 比较引用,直接修改原对象不会触发渲染。

  2. 在循环 / 条件 / 嵌套函数里调用 Hooks

    jsx
    // ❌ 违反 Hooks 规则
    if (cond) {
      const [x, setX] = useState(0);
    }

    必须在组件顶层调用,每次渲染顺序一致。

  3. useEffect 依赖数组漏写

    jsx
    useEffect(() => {
      fetch(`/api/${id}`); // 用了 id
    }, []); // ❌ 漏写 id,永远只请求第一次的 id
  4. setState 后立即读 state

    jsx
    setCount(count + 1);
    console.log(count); // ❌ 还是旧值!

    setState 异步,新值要在下次渲染时才生效。要用副作用读:useEffect(() => console.log(count), [count])

  5. 列表 key 用 index 动态增删时会引发"组件状态错位"(如 input 内容串到别的项)。

  6. JSX 必须有唯一根节点

    jsx
    return (
      <h1>A</h1>
      <h2>B</h2>  // ❌
    );
    // ✅ 用 Fragment
    return <><h1>A</h1><h2>B</h2></>;
  7. 事件处理写成调用而非引用

    jsx
    // ❌ 渲染时立即执行
    <button onClick={handleClick()}>...
    // ✅
    <button onClick={handleClick}>...
    <button onClick={() => handleClick(id)}>...
  8. 忘记清理副作用 订阅、定时器、WebSocket 都要在 useEffect 返回函数里清理,否则内存泄漏。


15. 一句话总结

React 让你只关心"界面长什么样"(声明式),用组件化搭积木,用 Hooks 管状态和副作用,剩下的脏活累活交给虚拟 DOM 和 Diff 算法。


16. 延伸阅读

  • 官方文档(中文):https://zh-hans.react.dev/
  • React 18 新特性:并发渲染、自动批处理、Suspense for Data
  • 下一章:第 12 章 React 进阶(自定义 Hooks / 性能优化 / Fiber)