最近在换工作,把 BOSS 直聘和其它招聘软件刷了一遍,发现一件事:招前端的岗位,十有八九写的是 React。
而我写了三年 Vue3,拿得出手的项目经验也基本都是 Vue 的------经验够,技术栈不对,就这么卡住了。
所以不是"React 更流行所以我学",是很现实的原因------岗位在那儿,我得过去。好消息是,你不是从零学,你只是换一套语法,把脑子里的心智模型平移过去。
有人会说现在 AI 都能写代码了,还学这些干嘛。我的答案是:AI 写的代码,总得有人看得懂。
这篇文章不教你 React 基础。它假设你懂 Vue3 的 ref、computed、watch,只做一件事:把你已经会的东西,逐项映射到 React。
通篇是一张对照表。
先说结论:映射关系
| Vue3 概念 | React 对应 | 相似度 |
|---|---|---|
ref / reactive |
useState |
高 |
computed |
useMemo |
高 |
watch / watchEffect |
useEffect |
中(差异最大) |
模板 ref 引用 DOM |
useRef |
中 |
vue-router useRoute |
react-router useParams |
高 |
class="x" |
className="x" |
完全等价 |
记住一条主线:Vue 帮你自动追踪依赖,React 要你手动声明依赖。这是后面所有差异的根源。
1. 状态:useState ≈ ref
Vue 里你这么写:
js
const count = ref(0)
count.value = 1
React 里是:
js
const [count, setCount] = useState(0)
setCount(count + 1)
对照:
| 维度 | Vue3 | React |
|---|---|---|
| 定义 | const count = ref(0) |
const [count, setCount] = useState(0) |
| 读值 | count.value |
直接用 count |
| 改值 | count.value = 1 |
setCount(count + 1) |
| 触发更新 | 改 .value 自动 |
调 setXxx 才更新 |
关键差别 :React 没有 .value。count 就是值本身,setCount 是唯一能改它的函数。你改了普通变量不会刷新界面------这是新手最常见的坑。
2. 副作用:useEffect vs watch / watchEffect(重点,差异最大)
2.1 React 基础用法
jsx
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
2.2 两类 effect:留没留下"活物"
判断一个 effect 要不要清理,问自己一句:"它执行完,往外面留下了活物吗?"
活物指:监听器、定时器、订阅、实例、请求。
- 无需清除的 effect :执行完就结束,不留下东西。比如上面改
document.title,跑完就没了。 - 需要清除的 effect:留下了订阅/监听,组件卸载时要手动断开,否则内存泄漏。
jsx
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
注意那个 return 的函数------它就是清理函数,组件卸载时 React 会调用它。
2.3 什么写法会导致无限循环
这是 React 新手掉得最狠的坑。先看懂本质,再记修复。
无限循环的本质 = 三个环节连成闭环:
- effect 执行:渲染之后自动跑
- setState 状态更新:没有这一步,链就断了,安全
- 重新渲染:组件函数再跑一遍,渲染后检查依赖:变了 → 回到①;没变 → 停
无限循环 = 闭环完整。修复思路只有一种:断掉其中一环。
最常见的三种成因:
| 成因 | 说明 | 断环方法 |
|---|---|---|
| 无依赖数组 + setState | effect 每次渲染都跑,一跑就 setState,又渲染,又跑... | 加依赖数组 [] |
| 依赖引用类型,每次渲染新建 | {a:1} 每次都是新引用,Object.is 永远不等 |
依赖原始值,或 useMemo 稳定引用 |
| setState 产生的新引用又进依赖 | 自己造的依赖自己改 | 用函数式更新 setX(x => x+1) |
2.4 Vue 这边:watch vs watchEffect
js
// watch:只追踪你明确写的数据源,依赖精确可控
watch(id, (newId, oldId) => {
// 仅 id 变才触发
})
// watchEffect:同步执行时自动追踪访问到的所有响应式属性
watchEffect(() => {
// 访问了谁就追踪谁,更方便但关系不那么明显
})
| 维度 | watch | watchEffect |
|---|---|---|
| 依赖来源 | 手动指定数据源 | 自动追踪访问到的响应式属性 |
| 触发时机 | 数据源确实改变才触发 | 同步执行期间自动收集 |
| 精确度 | 高,能精确控制 | 方便但依赖关系有时不明 |
2.5 Vue 清理副作用(3.5+)
js
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => { /* ... */ })
onWatcherCleanup(() => {
controller.abort() // 终止过期请求
})
})
老写法用回调的第三个参数:
js
watch(id, (newId, oldId, onCleanup) => {
onCleanup(() => { /* 清理逻辑 */ })
})
watchEffect((onCleanup) => {
onCleanup(() => { /* 清理逻辑 */ })
})
对照小结 :Vue 的 watch/watchEffect 更"聪明"(自动追踪),React 的 useEffect 更"笨"但更可控(依赖要你手写)。这是两种哲学,不是谁更好。
3. 派生数据:useMemo ≈ computed
Vue 的 computed:
js
const double = computed(() => count.value * 2)
React 的 useMemo:
js
const double = useMemo(() => count * 2, [count])
从三个维度对照:
| 维度 | Vue computed |
React useMemo |
|---|---|---|
| 依赖怎么来 | 自动追踪 | 手动声明数组 [count] |
| 依赖变了 | 重算 | 重算 |
| 用来稳定引用 | 基本不需要 | 经常需要(React 靠 Object.is 比引用) |
第三行是重点。React 里 useMemo 不止为了"算得快",更常为了"引用稳定" 。因为 React 默认用 Object.is 比较前后值,你每次渲染 new 一个对象/数组/函数,引用就变了,会触发不该触发的重渲染或 effect。这时候 useMemo(或 useCallback)就是用来把引用钉住的。
Vue 不需要这一步,因为响应式系统自己管引用。
4. useCallback 就是 useMemo 的语法糖
记住一句话:
js
useCallback(fn, deps)
// 等价于
useMemo(() => fn, deps)
它专门用来稳定函数 的引用。场景:你要把一个函数作为 prop 传给子组件,又不想每次渲染都造个新函数导致子组件无谓重渲染------用 useCallback 包一下。
5. useRef:跨渲染一直存在的盒子
js
const timer = useRef(null)
timer.current = setInterval(...)
useRef 是一个跨越渲染一直存在的盒子 。你改 timer.current,界面不会重新渲染。
这里要纠一个常见误解:useRef 不是 Vue 的 reactive。
reactive改了值 → 界面自动更新(响应式)useRef改了值 → 界面不动,它只是个能活过多次渲染的容器
useRef 真正用在两处:① 存定时器/订阅这种"不想触发渲染的变量";② 模板里拿 DOM 节点(等价于 Vue 的模板 ref)。
6. CSS:class → className(附一个真实踩坑)
Vue:
html
<div class="card"></div>
React:
jsx
<div className="card"></div>
就这一个词不一样。但有个真实坑我替你踩过:
在 Vite + React + TS 项目里,如果你写 import './taskListPage.css',编辑器可能报:
lua
src/pages/TaskListPage.tsx:4:39 - error TS2882:
Cannot find module or type declarations for side-effect import of './taskListPage.css'.
原因:TS 不认识 CSS 这种"副作用导入"。修法是新建一个 src/vite-env.d.ts,放一行:
ts
/// <reference types="vite/client" />
Vite 的类型声明就位后,TS 就知道 .css 导入是合法的了。这个文件 Vite 官方脚手架其实自带,自己建项目时容易漏。
7. 路由:useParams vs useRoute
Vue:
js
import { useRoute } from 'vue-router'
const route = useRoute()
route.params.id
route.query.tab
React(react-router v6/v7):
jsx
import { useParams, Link } from 'react-router-dom'
// 跳转:Link 只改 URL
<Link to={`/tasks/${taskNo}`}>详情</Link>
// 读取:在目标页用 useParams 取
const { taskNo } = useParams()
一个很多人绕不过去的弯 :<Link to> 负责"改 URL",useParams() 负责"读参数",两者是解耦 的。不是"用了 Link 跳转才配用 useParams"------任何匹配到 :taskNo 这个路由的页面,都能用 useParams 拿到它。跳转和取参是两条独立的线。
18 项速查表(收尾)
把上面拆细,就是你要的 18 项对照:
| # | 概念 | Vue3 | React | 关键差异 |
|---|---|---|---|---|
| 1 | 定义状态 | ref() |
useState() |
React 返回 [值, 改值函数] |
| 2 | 读状态 | x.value |
直接用 x |
React 无 .value |
| 3 | 改状态 | x.value = |
setX() |
React 只能靠 setter |
| 4 | 基础副作用 | watchEffect |
useEffect |
React 依赖手写 |
| 5 | 不清除的 effect | 执行完即止 | useEffect 无 return |
同理 |
| 6 | 需清除的 effect | onCleanup |
return () => {...} |
清理函数写法不同 |
| 7 | 无限循环成因 | 极少 | 三环闭环 | React 高频坑 |
| 8 | 无限循环修复 | --- | 断环三法 | 空依赖/原始值/函数式更新 |
| 9 | 精确依赖 | watch |
useEffect([deps]) |
都手动 |
| 10 | 自动依赖 | watchEffect |
(无原生对应) | React 需手动 |
| 11 | Vue 清理 3.5+ | onWatcherCleanup |
--- | Vue 专属 |
| 12 | 派生值 | computed |
useMemo |
都缓存 |
| 13 | 依赖来源 | 自动追踪 | 手动声明 | 核心差异 |
| 14 | 稳定引用需求 | 基本不需 | 经常需要 | React Object.is |
| 15 | 稳定函数 | --- | useCallback |
语法糖 |
| 16 | 跨渲染容器 | --- | useRef |
≠ reactive |
| 17 | DOM 引用 | 模板 ref |
useRef |
同用途 |
| 18 | 样式类名 | class |
className |
仅一词之差 |
7 天上手路线(可以直接照抄)
这是我自己的排期,每天大概 1-2 小时。顺序和节奏你都可以按自己的情况调,不用照搬。
| 天 | 内容 | 对应上表 |
|---|---|---|
| Day 1 | 心智切换:JSX + useState | 1--3 |
| Day 2 | useEffect 基础 + 清理 | 4--6 |
| Day 3 | 无限循环专题 | 7--8 |
| Day 4 | computed ↔ useMemo | 12--14 |
| Day 5 | useCallback + useRef | 15--17 |
| Day 6 | 路由 Link / useParams | 18 之外 |
| Day 7 | 综合:抄一个自己的小页 | 全部 |
Vue 和 React 不是两个语言,是两种"谁来管理依赖"的哲学。Vue 替你记,React 让你写。你 Vue3 的老底子在,平移过去最多一周。真正要小心的只有一处:React 的依赖要你亲手声明,漏了就无限循环,多了就白渲染。
把这张表存好。下次卡住,回来查第几行。
写在最后:这份对照表是我自己转 React 时边学边记的。接下来转型路上真正踩过的坑------Node、TS、部署这些,我还会继续写,学到哪儿写到哪儿,不按固定顺序。如果你也是从 Vue 过来的,欢迎在评论区说说你卡在哪儿,我可能正好也踩过。
另外也想问一句:AI 都能写 React 了,你觉得还有必要系统学一遍吗? 想听听大家的答案。