有了 Vue 基础快速入门 React已经会 Vue 了再学 React 其实很快——核心思想是一样的只是写法不同。这篇文章用 Vue 做参照把 React 的核心概念一个个对应起来看完就能上手写 React 项目。一、先搞清楚Vue 和 React 有什么不一样本质区别对比项VueReact模板写法template模板HTML 风格JSX直接在 JS 里写 HTML 结构响应式自动代理Proxy/Object.defineProperty改了就更新手动调用setState改了要手动通知组件写法Options API / Composition API函数组件 Hooks核心 APIref、reactive、computed、watchuseState、useEffect、useMemo、useCallback风格模板、逻辑、样式分离SFC逻辑和结构写在一起JSX最直观的感受Vue 像在写 HTML在模板里用v-if、v-for这些指令。React 像在写 JS用 JS 的if、map直接生成结构。一句话总结Vue 是「在 HTML 里掺 JS」React 是「在 JS 里掺 HTML」。二、创建项目Vuenpmcreate vitelatest my-vue-app ----templatevueReactnpmcreate vitelatest my-react-app ----templatereactVite 官方都有模板创建命令几乎一样只是--template后面从vue改成react。项目结构对比VueReactsrc/main.js入口src/main.jsx入口src/App.vue根组件src/App.jsx根组件.vue单文件组件.jsx或.tsx组件文件vite.config.jsvite.config.jsJSX 是什么JSX 是 JavaScript 的扩展允许在 JS 代码里直接写 HTML 标签。.jsx文件里的 HTML 标签最终会被编译成 JS 函数调用。三、组件的写法这是 Vue 和 React 最直观的区别。VueComposition APIscript setup import { ref } from vue const count ref(0) function increment() { count.value } /script template button clickincrement点击了 {{ count }} 次/button /template style scoped button { color: red; } /styleReactimport { useState } from react function Counter() { const [count, setCount] useState(0) function increment() { setCount(count 1) } return ( button onClick{increment} 点击了 {count} 次 /button ) } export default Counter对比一下VueReact说明script setup函数体逻辑都写在这里ref(0)useState(0)声明响应式数据count.valuesetCount(count 1)修改数据templatereturn (...)组件的 HTML 结构{{ count }}{ count }插值都是大括号clickonClick事件绑定style scopedCSS Modules / 内联 / 全局样式处理方式更多React 组件就是一个函数函数返回 JSX 就是组件的模板。每次状态变化这个函数会重新执行一遍返回新的 JSXReact 对比差异后更新 DOM。四、模板语法对比插值!-- Vue -- p{{ message }}/p{/* React */} p{ message }/p都是大括号Vue 是两对{{ }}React 是一对{ }。条件渲染Vue 用指令div v-ifisShow显示我/div div v-else显示他/divReact 直接用 JS 的三元表达式{ isShow ? div显示我/div : div显示他/div }如果只有if没有else用{ isShow div显示我/div }为什么 React 不做v-if这样的指令因为 React 的哲学是「JS 已经有 if 了为什么还要再造一个」。直接用 JS 原生语法不用学框架特有的指令。列表渲染Vue 用v-forul li v-foritem in list :keyitem.id{{ item.name }}/li /ulReact 用数组的map方法ul { list.map(item li key{item.id}{ item.name }/li) } /ulmap是 JS 数组原生方法遍历数组生成新数组。React 拿到一个包含 JSX 元素的数组就会全部渲染出来。key的作用和 Vue 里一样——给每个列表项一个唯一标识Diff 算法能更快找到变化。属性绑定Vue 用v-bind:或:img :srcimgUrl :alttitleReact 直接用大括号img src{imgUrl} alt{title} /React 里属性值写大括号就是 JS 表达式不写就是普通字符串。和 Vue 的:是一个意思。class 和 styleVuediv :class{ active: isActive } :style{ color: textColor }Reactdiv className{isActive ? active : } style{{ color: textColor }}注意两个坑class要写成className—— 因为class是 JS 的保留字style是一个对象—— 外层大括号是 JS 表达式内层大括号是对象字面量五、响应式数据这是 Vue 和 React 最大的区别也是最需要转变思维的地方。Vue 的响应式constcountref(0)// 直接改自动更新视图count.valueVue 用 Proxy 把数据代理了你改count.value的时候Vue 自动知道「数据变了该更新视图了」。React 的响应式const[count,setCount]useState(0)// 不能直接改必须调用 setCountsetCount(count1)React 没有自动代理。你必须调用setCount这个函数React 才知道「数据变了重新渲染组件吧」。直接改会怎样const[count,setCount]useState(0)// ❌ 错的视图不会更新countcount1// ✅ 对的setCount(count1)改了数据但没调用 setStateReact 不知道数据变了所以不会重新渲染。这是 Vue 转 React 最容易踩的第一个坑。对象和数组怎么改Vueconstuserreactive({name:张三,age:20})// 直接改属性就行user.age21Reactconst[user,setUser]useState({name:张三,age:20})// ❌ 错的user.age21// ✅ 对的创建一个新对象传进去setUser({...user,age:21})React 的 setState 是替换不是修改。你要给它一个全新的对象/数组它才会更新。数组也是一样const[list,setList]useState([1,2,3])// 追加setList([...list,4])// 删除第 i 个setList(list.filter((_,index)index!i))// 修改第 i 个setList(list.map((item,index)indexi?item1:item))核心原则永远返回一个新的数组/对象不要直接改原数据。这叫「不可变数据」Immutable是 React 的核心理念之一。六、生命周期 vs useEffectVue 的生命周期script setup import { onMounted, onUpdated, onUnmounted } from vue onMounted(() { console.log(组件挂载了) }) onUpdated(() { console.log(组件更新了) }) onUnmounted(() { console.log(组件卸载了) }) /scriptReact 的 useEffectimport { useEffect } from react function MyComponent() { // 相当于 onMounted onUpdated useEffect(() { console.log(组件挂载或更新了) }) // 相当于 onMounted只执行一次 useEffect(() { console.log(组件挂载了) }, []) // 相当于 onUnmounted清理函数 useEffect(() { return () { console.log(组件卸载了) } }, []) // 监听某个数据变化类似 watch useEffect(() { console.log(count 变了, count) }, [count]) return div{count}/div }对应关系表VueReact说明onMounteduseEffect(fn, [])依赖数组为空只执行一次onUpdateduseEffect(fn)不传依赖数组每次渲染都执行onUnmounteduseEffect(() { return () {} }, [])return 的函数是清理函数watch(count, fn)useEffect(fn, [count])监听 count 变化依赖数组是关键useEffect 的第二个参数叫依赖数组写法执行时机useEffect(fn)每次渲染都执行useEffect(fn, [])只在挂载时执行一次useEffect(fn, [a, b])挂载时执行 a 或 b 变化时执行为什么需要依赖数组因为 React 组件每次更新都会重新执行整个函数useEffect 也会重新跑。如果不加控制可能会出现死循环或者重复请求。一个经验法则useEffect 里用到了哪些 state/props就把哪些放进依赖数组里。ESLint 插件会提醒你。七、计算属性 vs useMemoVue 的 computedscript setup import { ref, computed } from vue const firstName ref(张) const lastName ref(三) const fullName computed(() { return firstName.value lastName.value }) /scriptReact 的 useMemoimport { useState, useMemo } from react function Name() { const [firstName, setFirstName] useState(张) const [lastName, setLastName] useState(三) const fullName useMemo(() { return firstName lastName }, [firstName, lastName]) return div{fullName}/div }区别Vue computedReact useMemo自动追踪依赖手动写依赖数组默认就有缓存只有用 useMemo 才缓存直接写就行简单计算不用 useMemo直接写在 JSX 里也行React 里什么时候用 useMemo计算量很大的时候用比如复杂的排序、过滤、大数据量处理。简单的字符串拼接、简单运算直接写就行不用特意包一层 useMemo。八、侦听器 vs useEffectVue 的 watchscript setup import { ref, watch } from vue const count ref(0) watch(count, (newVal, oldVal) { console.log(count 变了, oldVal, →, newVal) }) /scriptReact 里用 useEffect 代替import { useState, useEffect } from react function Counter() { const [count, setCount] useState(0) useEffect(() { console.log(count 变了, count) // 注意React 的 useEffect 拿不到 oldVal // 如果需要旧值要用 useRef 自己存 }, [count]) return div{count}/div }区别Vue 的 watch 能拿到新旧两个值React 的 useEffect 只能拿到新值。需要旧值的话得自己用 useRef 存上一次的值。九、父子组件通信父传子Vue!-- 父组件 -- Child :titletitle :countcount / !-- 子组件 -- script setup const props defineProps([title, count]) /scriptReact// 父组件 Child title{title} count{count} / // 子组件 function Child(props) { return div{props.title} {props.count}/div } // 或者解构 function Child({ title, count }) { return div{title} {count}/div }React 里 props 就是函数的第一个参数直接从参数里拿。和 Vue 的defineProps是一个意思。子传父Vue!-- 子组件 -- script setup const emit defineEmits([change]) emit(change, 100) /script !-- 父组件 -- Child changehandleChange /React// 子组件 function Child({ onChange }) { return button onClick{() onChange(100)}点击/button } // 父组件 Child onChange{handleChange} /React 没有emit这种机制。子传父的方式是父组件把一个函数通过 props 传给子组件子组件调用这个函数把数据当参数传过去。说白了就是Vue子组件emit一个事件 → 父组件监听事件React父组件传一个函数 → 子组件调用函数形式不同本质一样——都是子组件通知父组件「某事发生了这是数据」。十、事件处理绑定事件!-- Vue -- button clickhandleClick点击/button{/* React */} button onClick{handleClick}点击/button传参!-- Vue -- button clickhandleClick(id)点击/button{/* React */} button onClick{() handleClick(id)}点击/buttonReact 里如果直接写onClick{handleClick(id)}函数会在渲染时就执行了这不对。要包一层箭头函数。事件对象!-- Vue -- button clickhandleClick点击/button scriptfunction handleClick(e) { console.log(e.target) }/script{/* React */} button onClick{handleClick}点击/button scriptfunction handleClick(e) { console.log(e.target) }/script看起来一样但有个区别Vue 的事件对象是原生事件对象React 的是合成事件对象SyntheticEvent。不过日常使用时区别不大e.target、e.preventDefault()这些都能用。十一、双向绑定Vue 的 v-modelinput v-modelmessage /Vue 的v-model是语法糖等价于:valuemessageinputmessage $event.target.value。React 里没有 v-modelReact 没有双向绑定要手动写const [message, setMessage] useState() input value{message} onChange{e setMessage(e.target.value)} /为什么 React 不做 v-model因为 React 推崇「单向数据流」——数据从上往下传事件从下往上传。v-model 虽然写着方便但也隐藏了数据流向。React 选择让你显式地写出来数据流更清晰。当然表单多了手动写也很烦。实际项目中一般用表单库比如 React Hook Form、Formik来处理就像 Vue 项目用 VeeValidate 一样。十二、ref 的用法Vue 的 refVue 里ref有两个作用声明响应式数据const count ref(0)获取 DOM 元素或子组件实例div refboxReact 的 useRefReact 里useRef也有两个作用获取 DOM 元素import { useRef, useEffect } from react function MyComponent() { const inputRef useRef(null) useEffect(() { // 挂载后自动聚焦 inputRef.current.focus() }, []) return input ref{inputRef} / }和 Vue 的ref几乎一样只是访问时用.current。存一个不触发渲染的值const timerRef useRef(null) // 存定时器 timerRef.current setInterval(...) // 清除定时器 clearInterval(timerRef.current)useRef 的值改变了不会触发组件重新渲染。这和 Vue 的 ref 不一样——Vue 的 ref 改了会触发更新React 的 useRef 改了不会。什么时候用 useRef 存值存定时器 ID、DOM 引用、上一次的值这些「不需要渲染到页面上的数据」。十三、插槽 vs childrenVue 的插槽!-- 父组件 -- Card template #header h3标题/h3 /template p内容内容/p /Card !-- 子组件 Card.vue -- div classcard div classheader slot nameheader / /div div classbody slot / /div /divReact 的 children// 父组件 Card header{h3标题/h3} p内容内容/p /Card // 子组件 Card.jsx function Card({ header, children }) { return ( div classNamecard div classNameheader{header}/div div classNamebody{children}/div /div ) }对应关系默认插槽 →children一个特殊的 prop具名插槽 → 传普通的 prop值是 JSXReact 没有专门的插槽语法。因为 JSX 本身就是 JS传 JSX 元素当 props 就够了不需要额外的插槽机制。十四、路由Vue Router// router/index.jsimport{createRouter,createWebHistory}fromvue-routerconstroutes[{path:/,component:Home},{path:/about,component:About},{path:/user/:id,component:User},]constroutercreateRouter({history:createWebHistory(),routes})!-- 模板里 -- router-link to/首页/router-link router-view / !-- 编程式导航 -- script setup import { useRouter, useRoute } from vue-router const router useRouter() const route useRoute() router.push(/about) console.log(route.params.id) /scriptReact Router// App.jsx import { BrowserRouter, Routes, Route, Link } 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{User /} / /Routes /BrowserRouter ) }// 编程式导航 获取参数 import { useNavigate, useParams } from react-router-dom function User() { const navigate useNavigate() const params useParams() function goHome() { navigate(/) } return div用户 ID{params.id}/div }对应关系Vue RouterReact Routerrouter-linkLinkrouter-viewRoutesRouteuseRouter()useNavigate()useRoute()useParams()/useLocation()router.push(/a)navigate(/a)十五、状态管理Vue 用 Pinia// stores/user.jsimport{defineStore}frompiniaexportconstuseUserStoredefineStore(user,{state:()({name:张三,age:20}),actions:{growUp(){this.age}}})script setup import { useUserStore } from /stores/user const userStore useUserStore() console.log(userStore.name) userStore.growUp() /scriptReact 用什么React 生态里状态管理库比较多库特点类似 Vue 的Zustand轻量、简单上手最快Pinia 的感觉Redux Toolkit官方推荐生态完善略复杂Vuex 的感觉Jotai原子化状态管理小巧灵活—Context APIReact 内置不用装库但性能一般provide/inject新手推荐 Zustand写法最接近 Pinia// stores/user.jsimport{create}fromzustandexportconstuseUserStorecreate((set)({name:张三,age:20,growUp:()set(state({age:state.age1}))}))function UserInfo() { const { name, age, growUp } useUserStore() return ( div p{name}, {age}岁/p button onClick{growUp}长大/button /div ) }十六、一些容易踩的坑1. 改了数据视图不更新原因直接改了 state没有调用 setState。// ❌ 错 user.name 李四 // ✅ 对 setUser({ ...user, name: 李四 })2. useState 的初始值只生效一次function Child({ initialCount }) { // initialCount 只在第一次渲染时用后续 props 变化不会更新 count const [count, setCount] useState(initialCount) }如果需要跟随 props 变化用 useEffect 监听。3. 循环里的事件绑定要包箭头函数// ❌ 错渲染时就执行了 button onClick{handleClick(id)} // ✅ 对 button onClick{() handleClick(id)}4. class 要写成 className// ❌ 错 div classbox // ✅ 对 div classNamebox5. 组件名必须大写开头// ❌ 错React 会当成 HTML 标签 function counter() { return div / } // ✅ 对大写开头才是组件 function Counter() { return div / }6. JSX 只能有一个根元素// ❌ 错 return ( div1/div div2/div ) // ✅ 对包一个 Fragment return ( div1/div div2/div / ).../是React.Fragment的简写和 Vue 的template类似不会生成真实 DOM 节点。十七、学习路径建议有 Vue 基础学 React 其实是「换语法」的过程核心概念组件化、响应式、生命周期、虚拟 DOM都是一样的。最快上手路线用 Vite 建个 React 项目跑起来看看目录结构学 useState—— 对应 Vue 的 ref改数据要调用 setXxx学 JSX 写法—— 条件渲染用三元/、列表用 map、绑定用大括号学 useEffect—— 对应 Vue 的 onMounted watch学父子通信—— props 传下去回调函数传上来学 React Router—— 对应 Vue RouterAPI 换个名字而已选一个状态管理库—— 推荐 Zustand最像 Pinia写个小项目练手—— TodoList、商品列表、登录注册都可以一周左右就能上手干活了。前三天可能会觉得「好别扭还是 Vue 写着顺手」写一周就习惯了。两个框架都会了之后你会发现其实核心思想是一样的只是表达方式不同。总结一张对照表速查概念VueReact响应式数据ref()/reactive()useState()修改数据直接改.value调用setXxx()计算属性computed()useMemo()侦听器watch()useEffect(..., [x])挂载onMounteduseEffect(fn, [])卸载onUnmounteduseEffect(() () {}, [])模板templatereturn (...)插值{{ msg }}{ msg }条件渲染v-if/v-else三元表达式 /列表渲染v-forarray.map()事件绑定clickonClick属性绑定:srcsrc{...}双向绑定v-modelvalueonChange手动写父传子defineProps函数参数props子传父defineEmitsemit传回调函数子组件调用插槽slotchildrenpropref 拿 DOMrefxxxxxx.valueuseRef()xxx.current路由vue-routerreact-router-dom状态管理PiniaZustand / Redux单文件组件.vue.jsx/.tsx样式隔离style scopedCSS Modules / CSS-in-JS把这张表存下来写 React 的时候对照着看很快就能上手。