
1. 从零上手 Vue3 之前先把这几个概念理清楚很多人第一次接触 Vue3打开官网看到createApp、setup、ref、reactive这些词脑子是懵的。我当初也一样Vue2 的data、methods写得好好的突然告诉我换了一套写法心里多少有点抗拒。但真正用下来会发现Vue3 的组合式 API 不是为了不一样而不一样它解决的是 Vue2 在大型项目里逻辑复用困难、类型推导弱这两个老大难问题。先说清楚 Vue3 到底是个什么东西。它是 Vue.js 的第三个大版本核心变化集中在三块响应式系统从Object.defineProperty换成了Proxy、新增组合式 APIComposition API、更好的 TypeScript 支持。这三点决定了你后面写代码的方式也决定了面试里被问得最多的方向。那它适合谁学如果你是完全没碰过前端框架的小白Vue3 的上手曲线其实比 Vue2 更平缓因为官方脚手架create-vue已经把工程化配置都封装好了你不需要自己折腾 webpack。如果你是从 Vue2 转过来的重点要放在组合式 API 的思维转换上选项式 API 在 Vue3 里依然能用但新项目不建议再这么写。这篇文章我打算按真实的学习路径来组织先搭环境跑起来再理解响应式然后搞组件通信最后把路由和几个高频踩坑点讲透。每一块我都会说清楚为什么这么做而不是只丢一段代码让你抄。因为抄代码谁都会理解背后的逻辑才能在遇到问题时自己排查。提示本文所有示例基于 Vue 3.4 和 Vite 5如果你用的是其他版本个别 API 可能有细微差异遇到报错先看控制台提示的版本信息。2. 开发环境搭建别在第一步就卡住2.1 Node 版本选择与安装Vue3 的构建工具 Vite 对 Node 版本有要求Vite 5 需要 Node 18 或 20。我见过太多人用着 Node 14 去跑 Vite然后报一堆crypto.hash is not a function之类的错折腾半天以为是代码问题其实是环境版本太低。安装 Node 建议直接去官网下 LTS 版本装完之后在终端验证node -v npm -v两条命令都能正常输出版本号说明环境没问题。如果你电脑上已经有多个 Node 版本推荐用nvmMac/Linux或nvm-windows来管理切换版本一条命令的事比卸载重装省心得多。Windows 用户特别注意一点安装路径不要带中文和空格否则某些依赖在编译时会因为路径解析问题报错。这个坑我在帮别人排查时遇到过好几次路径改成纯英文就好了。2.2 用 create-vue 创建项目现在官方推荐的方式是create-vue它基于 Vite比老的vue-cli快很多冷启动基本在秒级。命令很简单npm create vuelatest执行后会有一系列交互式提问问你需不需要 TypeScript、JSX、Router、Pinia、ESLint 等。小白第一次建议这样选选项建议理由TypeScript先选 No不熟悉类型系统时先专注 Vue 本身JSXNo模板语法够用JSX 是进阶需求Vue RouterYes单页应用几乎必用PiniaYes状态管理后面会用到ESLintYes帮你提前发现低级错误PrettierYes统一代码风格选完之后进入目录、装依赖、启动cd 你的项目名 npm install npm run dev浏览器打开终端提示的地址通常是http://localhost:5173看到欢迎页面就说明环境通了。2.3 目录结构快速认识创建出来的项目结构大致是这样src/ ├── assets/ 静态资源 ├── components/ 公共组件 ├── router/ 路由配置 ├── stores/ 状态管理 ├── views/ 页面级组件 ├── App.vue 根组件 └── main.js 入口文件main.js是整个应用的起点Vue3 的挂载方式和 Vue2 完全不同import { createApp } from vue import App from ./App.vue import router from ./router const app createApp(App) app.use(router) app.mount(#app)Vue2 是new Vue({...}).$mount(#app)Vue3 改成了createApp工厂函数。这个改动的好处是每个应用实例相互隔离你可以在同一个页面里挂载多个 Vue 应用而互不干扰做微前端或者渐进式迁移时特别有用。注意app.use()必须在app.mount()之前调用顺序反了插件不生效页面白屏但控制台不一定报错这个坑很隐蔽。3. 响应式系统Vue3 最核心的那块拼图3.1 ref 和 reactive 到底该用哪个响应式是 Vue 的灵魂数据变了视图自动更新这是它区别于 jQuery 时代的根本。Vue3 提供了两个创建响应式数据的 APIref和reactive。新手最常问的就是这俩啥区别、啥时候用哪个。一句话概括ref什么类型都能包reactive只能包对象和数组。ref返回的是一个带.value属性的对象在模板里 Vue 会自动帮你解包所以模板里不用写.value但 JS 逻辑里必须写。import { ref, reactive } from vue const count ref(0) const user reactive({ name: 张三, age: 18 }) // JS 中修改 count.value // ref 要 .value user.age 20 // reactive 直接改 // 模板中 // {{ count }} 自动解包不用 .value // {{ user.name }}为什么ref要设计个.value因为 JavaScript 里基本类型数字、字符串是按值传递的没法直接追踪引用变化只能包一层对象通过 getter/setter 拦截。reactive底层用的是Proxy直接代理整个对象所以不需要.value。我的实际使用习惯是基本类型用ref一组相关的对象用reactive。但如果你团队里统一用ref也完全没问题反而少了一层这个到底要不要.value的心智负担。3.2 响应式丢失的三种典型场景这是 Vue3 里最容易踩的坑没有之一。数据明明改了视图就是不更新排查半天找不到原因。我总结了三类高频场景。第一类解构 reactive 对象const state reactive({ count: 0 }) let { count } state // 这样解构出来的是普通数字失去响应式 count // 视图不会更新解决办法是用toRefsimport { toRefs } from vue const { count } toRefs(state) count.value // 这样才行第二类直接替换整个 reactive 对象let state reactive({ list: [] }) state reactive({ list: [1, 2, 3] }) // 引用变了但模板还绑着旧的正确做法是改属性而不是换引用state.list [1, 2, 3]。第三类把响应式数据传给普通函数后修改const obj reactive({ a: 1 }) function change(o) { o { a: 2 } } // 重新赋值原对象没变 change(obj)这个本质是 JS 的引用传递问题不是 Vue 的锅但新手容易混淆。提示判断一个数据是不是响应式的可以用isRef()和isReactive()这两个工具函数调试时很有用。3.3 computed 和 watch 的取舍computed是计算属性有缓存依赖不变时不会重新计算watch是侦听器依赖变化就执行副作用。很多人分不清什么时候用哪个。判断标准很简单你要的是一个值就用 computed你要做一件事就用 watch。比如购物车总价这是值用 computedconst total computed(() { return cart.items.reduce((sum, item) sum item.price * item.qty, 0) })比如搜索关键词变化后要发请求这是副作用用 watchwatch(keyword, async (newVal) { const res await fetchSearch(newVal) results.value res.data })watch还有个deep选项侦听对象内部变化时需要开启但深度侦听性能开销大能不用就不用优先考虑用 computed 派生。4. 组件通信父子兄弟之间怎么传话4.1 父传子props 的正确打开方式父组件给子组件传数据用props。Vue3 里在script setup中通过defineProps声明// 子组件 Child.vue const props defineProps({ title: { type: String, required: true, default: }, count: Number })父组件这样用Child title你好 :count10 /注意count前面有冒号表示传的是表达式数字 10不加冒号传的是字符串 10。这个细节新手经常搞混导致类型校验报错。defineProps是编译宏不需要 import直接在script setup里用。它返回的对象是只读的子组件不能直接修改 props这是单向数据流原则改了 Vue 会在控制台警告。4.2 子传父emit 事件机制子组件要通知父组件用emit触发事件// 子组件 const emit defineEmits([update, delete]) function handleClick() { emit(update, { id: 1, name: 新值 }) }父组件监听Child updateonUpdate deleteonDelete /这里有个命名坑HTML 里事件名建议用 kebab-case短横线JS 里用 camelCase。虽然 Vue3 会自动转换但混用容易在排查时看花眼。如果子组件要修改父组件传下来的值标准做法是 emit 一个事件让父组件自己改而不是子组件直接改 props。这就是所谓的props down, events up。4.3 兄弟组件与跨层级provide/inject 和状态管理兄弟组件之间没有直接关系通信要么通过共同的父组件中转要么用状态管理。跨层级爷孙传值用provide/inject// 祖先组件 import { provide, ref } from vue const theme ref(dark) provide(theme, theme) // 后代组件 import { inject } from vue const theme inject(theme)provide/inject适合传递全局配置类的数据比如主题、语言。但如果是频繁变化的业务数据还是用 Pinia 更合适因为inject的数据流向不好追踪调试时容易懵。组件通信这块我整理了一张对照表场景方案适用性父传子props最常用子传父emit最常用跨层级provide/inject配置类数据任意组件Pinia复杂业务状态模板引用ref defineExpose调用子组件方法4.4 通过 ref 直接调用子组件方法有时候需要父组件主动调用子组件的方法比如让子组件弹窗打开。这时候用模板 refChild refchildRef /const childRef ref(null) function openDialog() { childRef.value.show() }子组件必须用defineExpose暴露方法否则父组件拿不到defineExpose({ show })这个方式虽然直接但耦合度高能不用就不用优先考虑用 props 和 emit 走数据流。5. 路由单页应用的骨架5.1 路由配置的基本结构Vue Router 4 是 Vue3 配套的路由库配置方式和 Vue2 时代略有不同。核心是创建一个路由实例import { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(/views/Home.vue) }, { path: /about, component: () import(/views/About.vue) } ] const router createRouter({ history: createWebHistory(), routes })createWebHistory是 HTML5 History 模式URL 干净没有#createWebHashHistory是 hash 模式URL 带#。生产环境用 History 模式需要服务端配置 fallback否则刷新页面会 404这是新手部署时最常见的坑。路由懒加载用() import(...)这样每个页面单独打包首屏只加载当前页面的代码速度明显提升。5.2 动态路由与参数传递带参数的路由用冒号声明{ path: /user/:id, component: UserDetail }组件里获取参数import { useRoute } from vue-router const route useRoute() console.log(route.params.id)注意useRoute和useRouter的区别useRoute拿的是当前路由信息只读useRouter拿的是路由实例用来跳转。名字差一个字母功能完全不同我见过不少人写错。跳转有两种方式声明式用router-link编程式用router.pushrouter.push(/user/1) router.push({ name: user, params: { id: 1 } }) router.replace(/login) // 不留下历史记录push和replace的区别在于是否往历史栈里加记录登录成功后跳转用replace避免用户点后退又回到登录页。5.3 路由守卫登录拦截的标准写法路由守卫用来在跳转前做拦截最典型的场景是登录校验router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })在路由配置里给需要登录的页面加meta{ path: /dashboard, component: Dashboard, meta: { requiresAuth: true } }这里有个细节next()必须调用且只能调用一次。忘记调用会导致导航卡住调用多次会报错。我调试时遇到过页面一直转圈不跳转最后发现是某个分支忘了写next()。5.4 路由跳转后组件不渲染的排查热词里有个router vue3 路由跳转 组件内容渲染不显示这个问题我实际遇到过。常见原因有三个一是router-view没放对位置或者被v-if条件挡住了二是路由配置的component路径写错控制台会有加载失败提示三是用了keep-alive但没配include导致组件被缓存后不刷新。排查顺序建议先看控制台有没有报错再看router-view是否存在最后检查路由配置。大部分情况是前两步就能定位。6. 几个高频踩坑点与实战建议6.1 v-if 和 v-for 不要一起用Vue3 里v-if的优先级高于v-for如果写在同一元素上v-if拿不到v-for的循环变量直接报错。正确做法是用template包一层或者用 computed 先过滤数据。!-- 错误 -- li v-foritem in list v-ifitem.active{{ item.name }}/li !-- 正确 -- template v-foritem in list :keyitem.id li v-ifitem.active{{ item.name }}/li /template6.2 样式穿透要用 :deep()在style scoped里想修改子组件或第三方库的样式Vue2 时代用::v-deep或/deep/Vue3 统一改成了:deep():deep(.el-button) { background: red; }不写:deep()的话scoped 会给选择器加上属性哈希匹配不到子组件内部的元素样式不生效。6.3 打包后白屏的常见原因本地跑得好好的npm run build之后部署上去白屏八成是这两个原因一是vite.config.js里的base没配部署到子目录时资源路径不对二是路由用了 History 模式但服务端没配 fallback。base配置export default defineConfig({ base: ./ // 相对路径适配任意部署目录 })6.4 关于 diff 算法和面试准备热词里vue3 diff 算法出现频率很高面试常问。简单说Vue3 的 diff 相比 Vue2 做了优化编译时标记静态节点diff 时跳过不变的部分最长递增子序列算法减少 DOM 移动次数。这些优化让 Vue3 在大列表更新时性能明显优于 Vue2。但我要提醒一句面试问 diff 算法重点不是让你背源码而是看你是否理解为什么需要虚拟 DOMkey 的作用是什么。key用 index 会导致列表更新错乱这个点几乎必问。6.5 从 Vue2 迁移的注意事项如果你手上有 Vue2 项目要迁移别想着一步到位。Vue3 支持选项式 API可以先升级依赖把new Vue改成createApp跑通之后再逐步把data、methods改写成setup。Vue.prototype.$xxx这种全局挂载要改成app.config.globalProperties.$xxx过滤器filter在 Vue3 里被移除了要用 computed 或方法替代。迁移过程中最容易出问题的是第三方库的兼容性尤其是那些依赖 Vue2 内部 API 的老库。升级前先查一下库的版本支持情况不支持的找替代方案。7. 学完之后怎么继续深入Vue3 的基础就这些但真正拉开差距的是工程化能力。建议下一步重点看三块Pinia 状态管理的模块化拆分、Vite 的构建配置与性能优化、TypeScript 在 Vue3 里的类型推导。这三块是后台管理系统类项目的核心也是面试里区分初级和中级的标志。我个人的体会是Vue3 的组合式 API 刚开始写会觉得别扭尤其是从 Vue2 转过来的人总想把data拆成一个个ref。但写多了会发现按功能组织代码比按选项组织代码清晰得多一个功能的逻辑全在一个setup块里不用在data、methods、computed之间来回跳。这个思维转变过来之后就回不去了。最后分享一个小技巧遇到不确定的 API直接去官网搜Vue3 的官方文档质量很高每个 API 都有在线示例。比在搜索引擎里翻二手教程靠谱得多。