一、React 前言总结
学习路线(我给你规划好)
🚀 第 1 阶段(必须)
- JSX
- useState
- 事件
- 列表 / 条件渲染
🚀 第 2 阶段(核心)
- useEffect(≈ watch + 生命周期)
- Props / 父子通信
- 受控组件
🚀 第 3 阶段(工程能力)
- 自定义 Hook
- Context
- React Router
- React Query / Zustand
1、先给你一个大局观(非常重要)
1️⃣ React 和 Vue3 的本质区别(一句话版)
Vue:框架(帮你规定好一切) React:库(你用 JavaScript 自己组织一切)
| 维度 | Vue3 | React |
|---|---|---|
| 定位 | 完整框架 | UI 库 |
| 响应式 | 自动(Proxy) | 手动(useState) |
| 模板 | template | JSX(JS 写 UI) |
| 数据流 | 双向绑定 | 单向数据流 |
| 思想 | MVVM | 函数式 UI |
👉 React 的核心哲学只有一句话:
UI = f(state) 视图是状态的函数
2、React 项目怎么起?(你先不用纠结)
Vue3(你熟的)
npm create vue@latest
React(现在主流)
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
你现在只需要记住:
- React 项目 100% 写 JS
- 没有
.vue文件 - 所有 UI 都是 JSX
3、React 最核心的东西:组件
1️⃣ Vue3 组件(你非常熟)
<script setup>
const count = ref(0)
</script>
<template>
<button @click="count++">
{{ count }}
</button>
</template>
2️⃣ React 组件(第一眼会不适应)
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
)
}
export default App
4、重点对照理解(一定要看)
1️⃣ React 组件 = 普通函数
function App() {
return <div>Hello</div>
}
👉 没有 data / methods / computed / watch
2️⃣ 状态:ref / reactive → useState
| Vue3 | React |
|---|---|
ref(0) | useState(0) |
.value | setter |
| 自动追踪 | 手动 set |
Vue
const count = ref(0)
count.value++
React
const [count, setCount] = useState(0)
setCount(count + 1)
声明一个状态 count,初始值是 0,并拿到一个修改它的方法 setCount
🚨 React 铁律
不能直接改 state
❌ count++
❌ state.xxx = 1
3️⃣ 模板 vs JSX(React 最重要的认知坎)
Vue template
<div>{{ count }}</div>
React JSX
<div>{count}</div>
👉 JSX 不是模板,是 JS 表达式 你可以直接写逻辑:
{count > 5 ? '大于5' : '小于等于5'}
5、事件绑定(Vue 同学最容易懵)
Vue
<button @click="add">+</button>
React
<button onClick={add}>+</button>
📌 注意三点:
- 驼峰命名:
onClick - 传函数,不是字符串
- 没有
this
6、条件 & 列表渲染(Vue → React 对照)
条件渲染
Vue
<div v-if="show">显示</div>
React
{show && <div>显示</div>}
列表渲染(非常重要)
Vue
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
React
{list.map(item => (
<li key={item.id}>{item.name}</li>
))}
👉 React 没有指令,只有 JS
7、React 最容易踩的 3 个坑(提前帮你避)
❌ 1. 想用双向绑定
React 没有 v-model
👉 你要写成:
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
❌ 2. 想在 render 里写副作用
❌ 发请求
❌ setState
❌ 操作 DOM
👉 要用 useEffect
太棒了,选择 useEffect 作为切入点非常明智。它是 React 中最强大但也最容易让 Vue 开发者感到困惑的地方。
在 Vue3 中,你有明确的生命周期钩子(onMounted, onUpdated, onUnmounted)和 watch。但在 React 中,这些功能全部由 useEffect 这一个 Hook 承包了。
二、了解 React 的“生命周期”(useEffect)
1. 核心心智模型:同步而非生命周期
- Vue3 的思路:组件到了某个阶段(挂载了、更新了),执行某个函数。
- React 的思路:副作用(Side Effect)与依赖(Dependencies)的同步。
useEffect的意思是:“当这些依赖项变化时,请重新运行这段逻辑。”
useEffect 的语法结构
useEffect(() => {
// 1. 这里是副作用逻辑 (例如 API 请求、订阅)
return () => {
// 2. 这里是清理逻辑 (Cleanup,相当于 onUnmounted)
};
}, [dependencies]); // 3. 依赖数组
2. Vue3 vs React 模式对照表
我们可以通过调整 useEffect 的第二个参数(依赖数组),来实现 Vue 中的不同钩子功能:
| Vue3 生命周期 / 监听 | React useEffect 写法 | 说明 |
|---|---|---|
onMounted | useEffect(() => { ... }, []) | 空数组。只在组件初次渲染后执行一次。 |
watch(source) | useEffect(() => { ... }, [source]) | 有值数组。当 source 变化时执行。 |
onUpdated | useEffect(() => { ... }) | 不传数组。每次组件渲染/更新后都执行(慎用)。 |
onUnmounted | useEffect(() => { return () => { ... } }, []) | 返回一个函数。该函数会在组件销毁前调用。 |
3. 重点突破:清理函数 (Cleanup)
在 Vue 中,你可能在 onMounted 里开启定时器,在 onUnmounted 里关闭。
在 React 中,这属于同一个逻辑块:
useEffect(() => {
const timer = setInterval(() => {
console.log('计时中...');
}, 1000);
// 清理函数:组件卸载 或 下次 Effect 执行前执行
return () => {
clearInterval(timer);
console.log('计时器已清除');
};
}, []); // 只在挂载时开启,卸载时关闭
4. 常见误区:为什么我的 useEffect 执行了两次?
你可能会发现,在开发环境下,useEffect 即使数组为空也会执行两次。
- 原因:React 的 Strict Mode (严格模式)。它会故意模拟“挂载 -> 卸载 -> 挂载”的过程,用来帮你检查是否有遗漏的“清理函数”(比如忘记关掉定时器或 WebSocket)。
- Vue 开发者视角:这在生产环境不会发生,不用担心。
5. 实战对比:请求数据
让我们看一个最常见的业务场景。
Vue3 做法
const data = ref(null);
onMounted(async () => {
data.value = await fetchData(props.id);
});
watch(() => props.id, async (newId) => {
data.value = await fetchData(newId);
});
React 做法 (更简洁)
const [data, setData] = useState(null);
useEffect(() => {
let isIgnore = false;
fetchData(id).then(res => {
if (!isIgnore) setData(res);
});
return () => { isIgnore = true; }; // 防止竞态条件
}, [id]); // 只要 id 变了,就重新获取数据,一行搞定 watch + mounted
三、受控组件
在 Vue3 中,我们习惯了用 v-model 走天下。它是一个“语法糖”,帮我们自动处理了变量绑定和事件监听。
在 React 中,没有 v-model**。你需要手动拆解这个过程,这就是所谓的“受控组件” (Controlled Components)**。
1. 核心概念:数据双向绑定的拆解
在 React 的哲学里,“数据流是单向的”。
- **Vue 的
v-model**:数据 <==> 视图(自动双向)。 - React 的受控组件:数据 ==> 视图,视图 --(通过事件)--> 修改数据(手动循环)。
2. 代码实现对比
让我们实现一个简单的输入框,同步显示用户输入的内容。
Vue3 (v-model)
<script setup>
import { ref } from 'vue'
const text = ref('')
</script>
<template>
<input v-model="text" />
<p>你输入了: {{ text }}</p>
</template>
React (受控组件)
你需要做两件事:
- 把
value绑定到状态。 - 绑定
onChange事件来手动更新状态。
import React, { useState } from 'react';
function InputExample() {
const [text, setText] = useState('');
const handleChange = (e) => {
// e.target.value 就是当前输入框的值
setText(e.target.value);
};
return (
<div>
<input
value={text} // 1. 绑定值 (类似 :value)
onChange={handleChange} // 2. 监听变化 (类似 @input)
/>
<p>你输入了: {text}</p>
</div>
);
}
3. 为什么要叫“受控”?
之所以叫“受控”,是因为这个表单元素的值完全由 React 的 State 控制。
- 如果你只写
value={text}而不写onChange,你会发现输入框锁死了,怎么敲键盘都没反应。 - 只有当你通过
setText更新了状态,React 重新渲染组件,输入框的值才会改变。
这虽然比 Vue 多写了两行代码,但它的好处是:你可以完全掌控输入。例如,你想让用户只能输入数字:
const handleChange = (e) => {
const val = e.target.value;
// 如果不是数字,直接不更新状态,输入框也就不会有变化
if (!isNaN(val)) {
setText(val);
}
};
4. 多个输入的处理技巧
在 Vue 中,你会写很多个 v-model。在 React 中,如果你有 10 个输入框,难道要写 10 个 handleChange 吗?
Vue 开发者的小技巧:利用对象的计算属性键名。
const [form, setForm] = useState({ name: '', age: '' });
const handleChange = (e) => {
const { name, value } = e.target;
// 注意:React 修改对象必须传入一个全新的对象(不可变性)
setForm({
...form, // 复制旧数据
[name]: value // 修改当前字段
});
};
// 渲染时
<input name="name" value={form.name} onChange={handleChange} />
<input name="age" value={form.age} onChange={handleChange} />
5. 什么时候用“非受控组件”?
偶尔你也会怀念原生 HTML 的操作,不想把每个输入都绑在状态上,React 提供了 useRef。
useRef类似于 Vue3 的const myRef = ref(null)。- 你可以通过
ref直接从 DOM 节点获取值,而不需要useState。这被称为**“非受控组件”**。
总结
- Vue:
v-model是自动驾驶,方便快捷。 - React: 受控组件是手动挡,虽然需要你亲自操作(
value+onChange),但在处理复杂逻辑(格式化、验证)时非常透明和灵活。
你已经掌握了状态、副作用和表单。接下来,要不要了解一下 React 中最让初学者头疼的“组件重新渲染”逻辑?或者我们直接聊聊如何处理组件间的状态共享(类似 Vuex/Pinia 的方案)?
四、JSX
既然你已经了解了受控组件,那我们必须深入聊聊 JSX。这是 React 的灵魂,也是很多 Vue 开发者一开始最不习惯的地方。
在 Vue 中,HTML、CSS 和 JS 是“各回各家,各找各妈”(SFC 模式)。而在 React 中,JSX 让我们直接在 JS 里写 HTML。
1. JSX 到底是什么?
JSX 看起来像 HTML,但它既不是字符串,也不是真正的 HTML。它是 JavaScript 的语法扩展。
当你写下 <h1>Hello</h1> 时,Babel 等编译器会把它转换成一个普通的 JS 函数调用:
// 你写的
const element = <h1>Hello</h1>;
// 编译器转成的 (React 17+ 之后)
import { jsx as _jsx } from "react/jsx-runtime";
const element = _jsx("h1", { children: "Hello" });
结论:JSX 本质上就是创建对象的表达式。这意味着你可以把 UI 赋值给变量、当作参数传递、或者从函数中返回。
2. Vue Template 与 JSX 的规则差异
虽然长得像,但 JSX 有几个“硬性规定”,这和 Vue 的模板语法不同:
① 必须有一个根元素 (Fragments)
在 Vue3 中,模板可以有多个根节点。但在 React 中,JSX 必须被包裹在一个元素内。如果你不想增加额外的 <div>,可以使用 Fragment (<>...</>)。
// ❌ 错误
return (
<Header />
<Content />
)
// ✅ 正确
return (
<>
<Header />
<Content />
</>
)
② 属性命名的“驼峰化”
因为 JSX 更接近 JS 而不是 HTML,所以 HTML 的属性名要改写成 JS 的属性名:
class➡️className(因为class是 JS 的关键字)for➡️htmlForonclick➡️onClickstyle="color: red"➡️style={{ color: 'red' }}(注意:这里是双大括号,外层是 JS 环境,内层是 JS 对象)
3. 在 JSX 中嵌入 JS (大括号的魔力)
在 Vue 中,你用 {{ }} 插值;在 JSX 中,你只用 **单括号 { }**。
大括号里可以写任何有返回值的 JS 表达式。
function Welcome() {
const name = "张三";
const formatName = (user) => user.firstName + user.lastName;
return (
<div title={name}> {/* 属性里也能用 */}
<h1>Hello, {name}</h1>
<p>计算结果: {1 + 1}</p>
<p>函数调用: {formatName({firstName: '王', lastName: '小二'})}</p>
</div>
);
}
4. 逻辑处理:没有指令,只有 JS
这是 Vue 开发者最需要转换思维的地方:**React 没有 v-if 或 v-for**。
条件渲染
使用 JS 的 && (与运算符) 或 ? : (三元运算符)。
{/* 相当于 v-if */}
{isLoggedIn && <LogoutButton />}
{/* 相当于 v-if / v-else */}
{isLoggedIn ? <Welcome /> : <Login />}
列表渲染
使用 JS 的 .map() 方法。记得带上 key!
const items = ['苹果', '香蕉', '橘子'];
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
5. 为什么 React 选择 JSX?
Vue 的模板更“静态”,Vue 编译器可以通过分析模板做很多自动化优化(如静态提升)。 React 的 JSX 更“动态”,它给了你 JS 的完整能力。
- Vue 思路:我为你提供了一套好用的工具箱(指令),你按规矩来。
- React 思路:我直接给你 JS 本身,你想怎么组合、怎么写逻辑,随你便。
五、事件
在 Vue3 中,我们使用 @click 或 v-on:click 来处理事件。在 React 中,事件处理依然遵循 “一切皆是 JS” 的原则。
掌握 React 事件,你只需要关注 3 个核心区别:命名方式、传参方式、以及事件对象。
1. 命名与绑定:从 @ 到 onCamelCase
- Vue: 使用小写命名,并配合指令,例如
@click、@keyup.enter。 - React: 使用 小驼峰 (camelCase) 命名,且直接传递函数引用。
对比代码:
Vue3:
<button @click="handleClick">提交</button>
React:
// 注意是 onClick,大写 C
<button onClick={handleClick}>提交</button>
注意: 在 React 中,一定要传递函数本身(
onClick={handleClick}),而不是函数的执行结果(onClick={handleClick()}),除非你的函数返回的也是一个函数。
2. 事件传参:没有指令,只有闭包
在 Vue 中,传参很简单:@click="say('hi')"。
在 React 中,如果你写 onClick={say('hi')},函数会在组件渲染时立即执行。
要在 React 中传参,你需要写一个匿名函数(箭头函数):
function App() {
const handleDelete = (id) => {
console.log('删除 ID:', id);
};
return (
// 使用箭头函数包装一下
<button onClick={() => handleDelete(123)}>删除</button>
);
}
3. 事件对象 (SyntheticEvent)
在 Vue 中,你可以通过 $event 获取原生 DOM 事件。
在 React 中,你得到的是一个 合成事件 (SyntheticEvent)。
React 为了跨浏览器兼容性,把原生事件包装了一层。它的接口和原生事件(e.preventDefault(), e.stopPropagation())一模一样,但它是经过 React 优化的。
const handleChange = (e) => {
console.log(e.target.value); // 获取输入框的值
e.preventDefault(); // 阻止默认行为(如表单提交刷新)
};
4. 常见的事件修饰符:手动实现
Vue 提供了非常方便的修饰符,如 .stop, .prevent, .once, .enter。
React 没有任何修饰符。你需要手动在函数体里写 JS 代码。
| 功能 | Vue 写法 | React 写法 |
|---|---|---|
| 阻止冒泡 | @click.stop | e.stopPropagation() |
| 阻止默认行为 | @submit.prevent | e.preventDefault() |
| 按键过滤 | @keyup.enter | if (e.key === 'Enter') { ... } |
示例:处理回车键
const handleKeyUp = (e) => {
if (e.key === 'Enter') {
console.log('用户按了回车');
}
};
<input onKeyUp={handleKeyUp} />
5. 为什么 React 没有修饰符?
这体现了 React 的设计哲学:“显式优于隐式”。
React 希望你清楚地知道代码在做什么,而不是隐藏在 HTML 指令的黑盒里。虽然多写了一行 e.preventDefault(),但代码的逻辑路径更加清晰。
老师的总结:一个经典的 React 事件处理套路
import { useState } from 'react';
function Form() {
const [val, setVal] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // 1. 手动阻止表单刷新
alert('提交成功: ' + val);
};
return (
<form onSubmit={handleSubmit}>
<input
value={val}
onChange={(e) => setVal(e.target.value)} // 2. 闭包+受控组件
/>
<button type="submit">提交</button>
</form>
);
}
六、父子通信
在 Vue3 中,父子通信靠的是 props 和 emit。在 React 中,逻辑更简单也更纯粹:一切皆为 Props。
React 遵循 “单向数据流”:数据从父传给子;如果子要改数据,就调用父传下来的“回调函数”。
1. 父传子:传递数据
这部分和 Vue 非常像。你在父组件的标签上写属性,子组件通过函数的第一个参数 props 接收。
Vue3 做法
<Child message="Hello" :count="10" />
<script setup>
const props = defineProps(['message', 'count'])
</script>
React 做法
在 React 中,我们通常直接用 对象解构 来获取 props,这样代码最简洁。
// 子组件
function Child({ message, count }) {
return (
<div>
<p>{message}</p>
<p>数量:{count}</p>
</div>
);
}
// 父组件
function Parent() {
return <Child message="来自父组件的消息" count={100} />;
}
2. 子传父:传递回调函数
这是 Vue 开发者最需要转换思维的地方。React **没有 $emit**。
- Vue 思路:子组件通过
emit('update')“大喊”一声,父组件监听。 - React 思路:父组件把一个函数像普通变量一样传给子组件,子组件在合适的时候调用这个函数。
代码实现:
// 子组件
function Child({ onIncr }) {
return (
// 当点击时,直接调用父组件传来的函数
<button onClick={() => onIncr(1)}>点我加 1</button>
);
}
// 父组件
function Parent() {
const [count, setCount] = useState(0);
const handleIncr = (step) => {
setCount(count + step);
};
return (
<div>
<h1>计数器:{count}</h1>
{/* 像传数据一样传函数 */}
<Child onIncr={handleIncr} />
</div>
);
}
3. Props 的“槽位”:Children (类似 Slot)
Vue 使用 <slot> 来传递插槽内容。React 使用一个特殊的 prop 叫做 children。
Vue3 (Slot)
<MyCard>
<p>我是插槽内容</p>
</MyCard>
React (Children)
任何夹在组件双标签中间的内容,都会被自动放入 props.children 中。
function MyCard({ children, title }) {
return (
<div className="card">
<h3>{title}</h3>
<div className="content">
{children} {/* 这里就是插槽内容 */}
</div>
</div>
);
}
// 使用
<MyCard title="公告">
<p>我是写在组件中间的内容,我会出现在 children 里。</p>
</MyCard>
4. Props 的只读性
和 Vue 一样,React 的 Props 是只读的。
- 错误操作:在子组件里直接修改
props.count = 10。 - 正确操作:如果需要修改,必须调用父组件通过 props 传下来的
setCount方法。
5. 跨级组件通信 (Provide/Inject vs Context)
如果你有很深的组件树(父 -> 子 -> 孙),一级级传 Props 会很痛苦(这叫 Props Drilling)。
- Vue3: 使用
provide和inject。 - React: 使用 Context API。
七、全局状态
八、路由(Routing)
既然我们已经聊到了全局状态,那么路由(Routing)就是把所有组件串联成一个完整应用的“骨架”。
在 Vue 中,vue-router 是官方深度集成的。在 React 中,虽然没有官方出品的路由,但 React Router 是事实上的标准,几乎 99% 的项目都在用它。
1. 核心概念对比:从 router-view 到组件化路由
Vue 的路由通常是配置驱动的(在 router/index.js 里写死一张表),而 React 的路由更倾向于组件驱动。
| 功能 | Vue Router | React Router (v6+) |
|---|---|---|
| 容器 | <router-view /> | <Outlet /> |
| 链接 | <router-link to="..."> | <Link to="..."> |
| 编程式导航 | router.push('/...') | useNavigate() |
| 动态参数 | $route.params.id | useParams() |
2. 基础路由配置
在 React 中,我们通常在 App.jsx 中定义路由树。
import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
</nav>
{/* 路由配置区域 */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* 动态路由示例 */}
<Route path="/user/:id" element={<UserDetail />} />
</Routes>
</BrowserRouter>
);
}
3. 嵌套路由:从 children 到 Outlet
在 Vue 中,嵌套路由会渲染在子组件的 <router-view /> 里。在 React 中,这个占位符叫 <Outlet />。
// 父级路由组件
function Layout() {
return (
<div className="admin-layout">
<aside>侧边栏菜单</aside>
<main>
{/* 相当于 Vue 的 <router-view /> */}
<Outlet />
</main>
</div>
);
}
// 路由配置
<Route path="/admin" element={<Layout />}>
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
4. 编程式导航与参数获取
作为 Vue 开发者,你最关心的可能是:如何在 JS 逻辑里跳转页面?如何获取 URL 里的 ID?
① 跳转页面 (useNavigate)
import { useNavigate } from 'react-router-dom';
function LoginPage() {
const navigate = useNavigate();
const handleLogin = () => {
// 登录成功后跳转,相当于 router.push('/dashboard')
navigate('/dashboard');
};
return <button onClick={handleLogin}>登录</button>;
}
② 获取参数 (useParams)
import { useParams } from 'react-router-dom';
function UserDetail() {
// 假设路径是 /user/123
const { id } = useParams();
return <div>用户 ID: {id}</div>;
}