前端开发这条路上Vue3 大概是这几年被问得最密集的一块。不管是刚转行做 web 前端开发的朋友还是写了几年 Vue2 想升级的老手绕不开的一个问题就是Vue3 到底该怎么系统学网上零散的 Vue3 入门教程很多但大多是讲一半要么只讲语法不讲工程化要么上来就甩一堆组合式 API 把人劝退。这篇我按自己的实际走法从环境搭建、核心语法、组件通信、路由状态管理一路写到后台管理系统实战和踩坑排查尽量把从入门到熟练使用这条线拉通。内容偏实战适合零基础想入门 Vue3 的、Vue2 转 Vue3 的、以及准备前端开发面试题想查漏补缺的同学。看完你至少能独立搭一个能跑的项目并且知道遇到问题该往哪个方向查。1. 先把 Vue3 是什么、能干什么讲明白1.1 从操作 DOM 到操作数据思路得先换过来很多人学 Vue3 卡住不是卡在语法是卡在思路。早些年写页面习惯是我要改这个按钮的文字于是先document.getElementById拿到元素再改innerText。这套写法在页面小的时候没问题一旦页面上有十几个地方要联动代码就会变成一团互相调用的面条。Vue3 的核心思路反过来你只管维护数据数据变了页面自己会跟着变。这种机制叫响应式。举个生活里的例子传统 DOM 操作像是你手动去拨钟表的指针指针走到几点全靠你拨响应式像是钟表接了电时间这个数据一变指针自己就转过去了。所以学 Vue3 的第一件事是强迫自己不要再去想怎么改 DOM而是想哪个数据变了谁依赖这个数据。这个思维转换过来之后后面的ref、reactive、computed、watch全都会顺理成章。1.2 三个必须搞懂的核心概念Vue3 的东西看着多但骨架就三根响应式系统、组件化、单文件组件SFC。响应式系统负责数据变页面变这是 Vue 的发动机。Vue3 用Proxy重写了这部分Vue2 用的是Object.defineProperty带来的直接好处是可以监听数组下标赋值、可以监听对象属性的新增和删除、性能也更好。你在 Vue2 里写this.arr[0] x页面不更新、必须用$set的那种憋屈在 Vue3 里基本没有了。组件化是把页面拆成一块块可复用的积木。一个按钮、一个表格、一个弹窗都可以是组件。组件之间通过 props 往下传数据、通过 emit 往上抛事件这是 Vue 生态里最基础的通信方式。单文件组件就是那个.vue文件一个文件里同时写script、template、style逻辑、结构、样式三件套放一起改起来不用满项目找文件。这是 Vue 相比 React 在新手友好度上最大的优势之一。1.3 学习顺序别搞反这几样可以往后放新手最容易犯的错是什么都想一次学完结果Teleport、Suspense、自定义渲染器、h函数全塞进脑子里两天就放弃了。我的建议是按这个顺序走阶段该学的内容可以暂时跳过第一周模板语法、ref、computed、v-for、v-ifdefineAsyncComponent、Suspense第二周组件拆分、props/emit、插槽、生命周期自定义指令、渲染函数第三周Vue Router、Pinia、axios 封装SSR、Nuxt第四周完整项目实战、打包部署源码级 diff 算法这个顺序的逻辑很简单先把能写出一个页面的最小闭环搭起来再往上加工程化的东西。一上来就啃mini-vue源码实现属于顺序反了容易劝退。提示官方文档是第一手资料遇到 API 细节拿不准直接查官方文档比搜二手博客靠谱得多二手文章经常停留在旧版本写法。2. Windows 下把 Vue3 开发环境一次装好2.1 Node.js 与包管理器的选择Vue3 项目离不开 Node.js因为构建工具、依赖安装全靠它。去 Node.js 官网下 LTS 版本就行别追求最新版LTS 更稳。装完之后开命令行验证node -v npm -v两条命令都能打出类似v20.x.x和10.x.x的版本号说明装好了。这里有个新手常踩的坑装完 Node 之后必须重开一个命令行窗口否则环境变量没刷新敲node -v会提示不是内部或外部命令。包管理器方面npm 是自带的够用。但我个人更推荐pnpm原因是它用硬链接共享依赖装包速度快、磁盘占用小。装法npm install -g pnpm pnpm -v如果公司项目要求统一用 npm那就别折腾跟着团队走。工具选型这件事团队一致性比个人偏好重要。2.2 用 Vite 创建项目五分钟跑起来早些年用vue-cli建项目基于 webpack启动一次要等十几秒。现在主流是Vite冷启动基本一两秒热更新几乎是即时的。创建命令pnpm create vite my-vue-app --template vue cd my-vue-app pnpm install pnpm dev--template vue表示用 JavaScript 版本如果你确定要上 TypeScript就用--template vue-ts。我的建议是第一次学 Vue3 用 JS 版本别一上来 JS 和 TS 一起学两套心智负担叠加容易崩。等语法熟了再切 TS 版本那时候你会发现 TS 带来的类型提示确实香但它是加分项不是入门门槛。跑起来之后浏览器打开终端里提示的地址一般是http://localhost:5173看到 Vue 的欢迎页就算成功了。2.3 VS Code 插件与怎么看网页界面由什么代码构成做前端开发VS Code 基本是标配。必装的插件就两个Vue - Official原 Volar提供.vue文件的语法高亮、模板内补全、类型提示。装了它之后一定要把旧的 Vetur 禁用掉两个插件同时开会在模板里报一堆莫名其妙的错。ESLint Prettier代码规范和格式化团队协作必备。然后回答一个被问得特别多的热词问题做网页前端开发怎么看一个网页界面是由什么代码构成的答案是浏览器自带的开发者工具。在页面上按F12或者右键选检查打开 Elements 面板。鼠标在上面移动页面上对应区域会高亮反过来在页面上右键检查也能直接定位到那个元素。这个操作要养成肌肉记忆因为它解决的问题是你看到一个效果想知道它是怎么实现的。具体几个常用用法。一是看样式选中元素后右侧 Styles 面板会列出所有生效的 CSS 规则被划掉的表示被覆盖了这能帮你快速定位样式冲突。二是看层级div套div套了几层一眼能看出来很多点了没反应的问题就是因为上面盖了一个透明的遮罩层。三是看网络Network 面板能看到每个请求的状态码、返回内容、耗时接口联调时全靠它。VS Code 这边虽然也能通过 Live Preview 之类的插件预览页面但调试能力远不如浏览器原生工具。我的习惯是写代码在 VS Code调样式和查接口在浏览器 F12两个窗口并排放。2.4 项目目录结构逐个说清楚Vite 创建出来的项目长这样my-vue-app ├── public/ 静态资源原样拷贝到打包目录 ├── src/ │ ├── assets/ 需要被构建处理的资源比如图片、字体 │ ├── components/ 公共组件 │ ├── App.vue 根组件 │ └── main.js 入口文件 ├── index.html 页面模板 ├── vite.config.js 构建配置 └── package.json 依赖与脚本几个关键点解释一下。main.js是入口里面的createApp(App).mount(#app)干的事就是创建一个 Vue 应用把它挂到index.html里 id 为app的那个节点上。index.html里那个div idapp/div就是整个应用的容器。public和assets的区别经常有人搞混public里的文件会被原封不动复制到打包输出目录必须用绝对路径引用比如/logo.pngassets里的文件会被 Vite 处理压缩、加哈希、转 base64需要用import或相对路径引用。放图片时想清楚要不要走构建处理选错了会出现开发环境能显示、打包后 404的经典问题。3. 核心语法打地基这四组 API 决定你写得多顺3.1 模板语法与常用指令先混个脸熟模板就是template里写的那部分它长得像 HTML但多了指令。核心的指令其实没几个script setup import { ref } from vue const msg ref(你好) const list ref([苹果, 香蕉, 橙子]) const show ref(true) const handleClick () { msg.value 被点了 } /script template p{{ msg }}/p button clickhandleClick点我/button ul li v-for(item, index) in list :keyindex{{ item }}/li /ul p v-ifshow这行会显示/p /template{{ }}是插值把数据显示到页面上。click是v-on:click的简写绑定事件。v-for做列表渲染:key前面那个冒号是v-bind的简写表示绑定一个动态值而不是写死的字符串。v-if控制元素是否渲染。这里有两个新手必踩的坑。第一v-for的:key不要随便用 index如果列表会做增删排序用 index 会导致复用错位尽量用数据里唯一的 id。第二v-if和v-for不要写在同一个元素上Vue3 里v-if的优先级比v-for高你在v-if里想用v-for的循环变量是拿不到的正确做法是在外层套一个template v-for再在里面写v-if。3.2 ref 和 reactive 到底用哪个这是组合式 API 里最让人迷糊的一对。简单说ref什么类型都能装reactive只能装对象和数组。import { ref, reactive } from vue const count ref(0) const user reactive({ name: 张三, age: 18 }) count.value // ref 要 .value user.age 19 // reactive 直接改注意那个.value。很多人问为什么ref非要加.value模板里又不用加原因是ref返回的是一个带.value属性的包装对象靠这个包装才能对基本类型做响应式追踪而模板里 Vue 会自动帮你解包所以写起来没感觉。那到底怎么选我自己的经验是统一用ref只在明确需要一个对象聚合、且不想到处写.value的时候用reactive。理由是ref可以被整体替换count.value 新对象reactive一旦被整个替换就丢失响应性了另外ref传递和解构时更安全reactive直接解构会丢响应性得配合toRefs。统一用ref能减少团队协作时的决策成本。注意reactive返回的对象const { name } user这样解构出来的是普通字符串改它不会触发更新必须const { name } toRefs(user)。3.3 computed 和 watch 的取舍这两个都是根据数据变化做点事但定位完全不同。computed是计算属性本质是派生数据。它的结果会被缓存依赖不变就不会重新计算适合做数据加工过滤数组、拼接展示文本、计算总价。watch是侦听器本质是副作用。它不产生新值而是数据变化时去执行一段逻辑发请求、写本地存储、操作 DOM、调第三方库。维度computedwatch是否有返回值有返回新值没有只执行逻辑是否缓存有缓存无缓存变一次跑一次典型场景总价、过滤后的列表、状态文案请求接口、存 localStorage、同步父组件能否异步不适合适合但要注意竞态竞态是watch里最容易被忽略的坑。比如搜索框输入关键词后发请求输入快了会连续发好几个请求返回顺序不保证可能出现后发先至页面显示的是旧关键词的结果。解决办法是做防抖或者给每次请求打个序号只接受最后一次的结果。computed也有个隐藏坑不要在computed里做赋值或者发请求。它应该是纯粹的输入到输出一旦掺进副作用缓存逻辑就会变得不可预测调试能把你逼疯。3.4 生命周期钩子记住常用的那几个就够组件从创建到销毁会经历一系列阶段每个阶段有对应的钩子函数。组合式 API 里这些钩子都以on开头script setup import { onMounted, onUnmounted, onUpdated } from vue onMounted(() { console.log(组件挂载完成DOM 可以访问了) window.addEventListener(resize, handleResize) }) onUpdated(() { console.log(数据更新导致 DOM 更新完成) }) onUnmounted(() { window.removeEventListener(resize, handleResize) console.log(组件销毁清理工作在这里做) }) /script实际项目里用得最多的是onMounted和onUnmounted。前者用来初始化请求首屏数据、初始化图表实例、绑定全局事件。后者用来清理解绑事件、清除定时器、销毁第三方实例。我见过太多内存泄漏的案例根源都是绑了没解。典型场景是在onMounted里setInterval轮询接口组件销毁了定时器还在跑越切页面越卡。绑定的东西必须在onUnmounted里对应解绑这条要刻进习惯里。4. 组件化开发把页面拆开再装回去4.1 父子通信的四种常用姿势组件通信的核心就一句话数据往下传事件往上传。!-- 子组件 Child.vue -- script setup const props defineProps({ title: String, count: Number }) const emit defineEmits([update]) const handleClick () emit(update, props.count 1) /script template div h3{{ title }}/h3 button clickhandleClick加一/button /div /template!-- 父组件 -- script setup import { ref } from vue import Child from ./Child.vue const num ref(0) /script template Child title计数器 :countnum updateval num val / /templatedefineProps声明父组件传进来的数据defineEmits声明可以往外抛的事件。这两个宏不需要 import在script setup里直接用。除了这套基础玩法还有两个进阶的v-model双向绑定本质是 props 加 emit 的语法糖和插槽 slot。插槽解决的是内容由父组件决定、布局由子组件决定的场景比如做一个弹窗组件弹窗的壳子固定里面放什么内容交给调用方。默认插槽、具名插槽、作用域插槽三兄弟作用域插槽最有用也最容易忘它能让你在父组件里拿到子组件的数据来渲染。经验props 是只读的子组件里直接改 props 会报警告。要改的话要么 emit 出去让父组件改要么在子组件里用computed包一层做本地化。4.2 provide/inject 和状态提升怎么选当层级比较深的时候比如祖孙组件用 props 一层层传会非常难受业界叫prop drilling。这时候有两种方案。一种是provide/inject祖先组件提供数据任意深度的后代都能注入// 祖先 import { provide, ref } from vue const theme ref(dark) provide(theme, theme) // 后代 import { inject } from vue const theme inject(theme)另一种是状态提升把共享数据提到最近公共祖先或者干脆交给 Pinia 全局管理。怎么选我的判断标准是如果这个数据只在一个局部树里共享比如一个表单页和它内部的若干子组件用 provide/inject 就够如果这个数据跨页面、跨模块比如登录用户信息、权限列表、主题配置那别犹豫直接上 Pinia。provide/inject 有个缺点数据从哪来不好追溯调试时看到inject(theme)得满项目搜provide(theme)。所以我在团队里会约定provide 的 key 统一在一个常量文件里定义别用裸字符串。4.3 Vue Router单页应用的路由骨架Vue 做的是单页应用页面切换实际是组件替换这套机制靠 Vue Router 实现。安装和配置pnpm add vue-router4// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(../views/Home.vue) }, { path: /login, component: () import(../views/Login.vue) }, { path: /user/:id, component: () import(../views/User.vue) } ] const router createRouter({ history: createWebHistory(), routes }) export default router这里用() import()做的叫路由懒加载效果是访问到那个路由时才加载对应代码首屏体积能小一大截。做后台管理系统的时候这一招基本是必用的。路由守卫是另一个重点用来做登录拦截router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })这段逻辑看着简单但新手常踩的坑是死循环如果/login页面也被守卫拦了就会一直跳登录页。所以判断里一定要排除登录页本身。另外要小心next()被调用多次Vue Router 会报错。4.4 Pinia比 Vuex 好用不止一点点Vue3 时代的状态管理首选Pinia它是官方推荐的 Vuex 替代品。优势很直接没有 mutations 那层嵌套state、getters、actions三段式清爽TypeScript 支持也好得多。// src/stores/user.js import { defineStore } from pinia import { ref, computed } from vue export const useUserStore defineStore(user, () { const token ref() const userInfo ref(null) const isLogin computed(() !!token.value) async function login(form) { // 假装调接口 const res await fetch(/api/login, { method: POST, body: JSON.stringify(form) }) const data await res.json() token.value data.token userInfo.value data.user } function logout() { token.value userInfo.value null } return { token, userInfo, isLogin, login, logout } })这种组合式写法的 Pinia store看起来就跟写一个普通组件差不多很符合 Vue3 的直觉。用的时候import { useUserStore } from /stores/user const userStore useUserStore() console.log(userStore.isLogin)有个细节要注意store 的实例要在setup作用域里调用别在模块顶层直接useUserStore()然后到处引用容易出初始化顺序问题。5. 实战从零搭一个后台管理系统雏形学完语法不动手一周就忘光。我建议第一个实战项目就做后台管理系统因为它把路由、权限、表格、表单、请求封装全都囊括了是目前前端开发岗位上需求量最大的项目类型之一。5.1 登录页先把风格做出彩后台系统第一眼就是登录页。现在挺流行的一种做法是点线动态背景一堆小点在页面上缓慢漂移距离近的点之间连一条线鼠标靠近时线条会跟着变化。这个效果不复杂用 Canvas 画就行核心思路三步生成 N 个随机位置的点、每帧让点按速度移动并反弹边界、双重循环计算点之间的距离小于阈值就画线线的透明度按距离线性插值。关键代码大概这样function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height) points.forEach(p { p.x p.vx p.y p.vy if (p.x 0 || p.x canvas.width) p.vx * -1 if (p.y 0 || p.y canvas.height) p.vy * -1 ctx.beginPath() ctx.arc(p.x, p.y, 2, 0, Math.PI * 2) ctx.fill() }) for (let i 0; i points.length; i) { for (let j i 1; j points.length; j) { const d Math.hypot(points[i].x - points[j].x, points[i].y - points[j].y) if (d 120) { ctx.globalAlpha 1 - d / 120 ctx.beginPath() ctx.moveTo(points[i].x, points[i].y) ctx.lineTo(points[j].x, points[j].y) ctx.stroke() } } } requestAnimationFrame(draw) }有两个实际要注意的地方。一是点的数量要控制超过 150 个点双重循环就是上万次距离计算低配机器会掉帧一般 60 到 100 个就够看了。二是必须在组件卸载时取消动画用onUnmounted里cancelAnimationFrame停掉否则路由切走了它还在后台空转这就是前面说的绑了没解的典型。5.2 布局、菜单和 Tabs 标签页样式改造后台管理系统的布局基本是固定的左侧菜单栏、顶部面包屑和用户信息、中间内容区。内容区上面常常加一排Tabs 标签页点过的菜单会在这里留下标签方便来回切。默认样式的 Tabs 一般不够好看改样式时就会遇到一个高频问题组件库的样式改不动。原因是组件库的样式带作用域你在style scoped里写的选择器命中不了。解决办法是用深度选择器:deep()style scoped :deep(.el-tabs__item) { font-size: 13px; height: 34px; line-height: 34px; } :deep(.el-tabs__active-bar) { height: 2px; } :deep(.el-tabs__nav-wrap::after) { display: none; } /style:deep()的含义是穿透 scoped 限制往子组件内部找。用它的原则是只穿透到必要的层级别写:deep(.el-tabs .el-tabs__nav .el-tabs__item span)这种超长链组件库一升级类名变了就全废。心得改组件库样式之前先按 F12 看一眼实际的类名和层叠关系很多时候不用:deep()只是你的选择器权重不够加个父级类名就够了。5.3 请求封装别在每个组件里裸写 axios新手写项目最容易出现的代码是每个页面里都import axios然后重新配一遍 baseURL、重新写一遍错误处理。这在后期是灾难接口域名换了要改几十个文件。正确做法是统一封装// src/utils/request.js import axios from axios const service axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) config.headers.Authorization Bearer ${token} return config }) service.interceptors.response.use( res { const { code, data, message } res.data if (code 200) return data if (code 401) { localStorage.removeItem(token) location.href /login } return Promise.reject(new Error(message || 请求失败)) }, err { if (err.code ECONNABORTED) console.error(请求超时) return Promise.reject(err) } ) export default service这里有几个设计考虑值得说。baseURL用import.meta.env.VITE_API_BASE从环境变量读这样开发、测试、生产环境可以用不同的配置文件不用改代码。请求拦截器统一带 token省得每个接口手动传。响应拦截器统一处理业务错误码把data直接返回出去业务层拿到的就是干净的数据不用写res.data.data这种套娃。超时时间设 10 秒是有讲究的太短了网络稍微抖一下就报错太长了用户等得心焦。除了文件上传这种特殊接口单独调大其余接口 10 到 15 秒比较平衡。5.4 打包与部署最后这一公里开发完了要打包pnpm build产物在dist目录。这时候常见的问题来了部署到服务器后刷新页面 404。原因是单页应用只有index.html一个入口路由是前端控制的服务器上并不存在/user/123这个真实文件。解决办法是在服务器配置里加一条回退规则把找不到的路径统统指回index.html。Nginx 的写法是location / { try_files $uri $uri/ /index.html; }另一个常见问题是资源路径不对尤其是部署在子目录下时。这时候要在vite.config.js里设置baseexport default defineConfig({ base: /my-app/, plugins: [vue()] })打包体积也是个绕不开的话题。第一招是路由懒加载前面说过。第二招是看构建产物分析把体积异常的依赖找出来。第三招是把 echarts、moment 这类大库按需引入moment 尤其坑换成 dayjs 能省一大截。我见过一个项目因为全量引入了 echarts 又没做拆分首屏包 2MB 多优化完降到 600KB 左右体验差别非常明显。6. 常见坑与排查技巧实录6.1 环境和依赖类问题速查这类问题的特点是你代码没写错但项目就是起不来。我把常遇到的整理成表现象大概率原因处理办法敲 node 提示不是内部命令装完没重开终端关掉命令行重开装依赖卡住不动镜像源慢或网络问题切国内镜像源启动报 EADDRINUSE端口被占用换端口或杀掉占用进程多人协作后依赖报错lock 文件冲突删掉 node_modules 重新装TS 项目满屏类型报错依赖版本或 tsconfig 不匹配检查版本对齐别乱升关于类型报错有个很典型的场景在若依、jeecg 这类基于 Vue3 TypeScript 的后台管理系统脚手架上做二次开发改动依赖后突然出现一堆类型报错。这种情况九成是依赖版本没对齐或者tsconfig.json里的types配置少了东西。我的处理顺序是先看报错信息指向哪个库再看package.json里这个库和它的类型包版本是否一致最后再怀疑自己写的代码。别一上来就怀疑业务逻辑。6.2 响应式失效的几种典型表现数据改了页面不动是 Vue 新手最常见的困惑。按我遇到的频率排第一种解构丢了响应性。前面说过reactive直接解构会失效ref解构也会失效。解决办法是用toRefs或者干脆不解构。第二种把值存进了普通变量。比如const c count.value之后改count.valuec永远不变因为它就是个普通数字拷贝。想保留联动就必须保留.value的引用。第三种整体替换了 reactive 对象。let state reactive({...})然后state reactive({...})重新赋值原来的引用断了。要整体替换就用ref再state.value {...}。第四种数组操作用了下标赋值或改 length。Vue3 用 Proxy 之后这个其实已经修好了但如果你的响应式数据来源是Object.freeze过的配置对象还是会失效。排查思路很简单先确认数据真的变了在修改后console.log一下再用 Vue Devtools 看组件里的值有没有跟着变。数据变了但视图没变才叫响应式问题数据压根没变那是逻辑问题两回事。6.3 第三方库和样式的两个经典冲突第一个pxtorem 对 ECharts 没效果。这个问题在 Vue3 项目里被问得非常多。原因其实很好理解postcss-pxtorem 是在 CSS 编译阶段把px转成rem而 ECharts 是画在 Canvas 上的它的尺寸、字体大小都是 JavaScript 运行时算出来的数值压根不经过 CSS 编译。所以插件对它无效这不是配置写错了。解决办法有三个按推荐度排一是给 ECharts 的容器用百分比或vw宽度让它自己适应二是根据根节点的font-size手动换算写个工具函数把设计稿的 px 值转成实际需要的数值在option里用三是监听窗口 resize在回调里重新计算并setOption同时调用chart.resize()。我一般用第一种加第三种组合最省事。第二个浏览器卡到连窗口按钮都点不动。有同学反馈 Vue3 项目在 Edge 里偶尔会出现右上角最小化按钮点不了的情况。这个现象听起来像浏览器的问题实际上绝大多数是主线程被阻塞了。浏览器窗口的按钮响应也依赖主线程如果你的代码里有死循环、超大的同步计算、或者一次渲染了几万个 DOM 节点主线程就一直被占着浏览器自然僵住。排查方向是打开性能面板录制一段看有没有超长的任务。常见的元凶有watch里写了会互相触发的逻辑形成死循环、大数组渲染没做虚拟列表、递归函数没写好边界条件。找到之后做三件事把大计算拆成小块用requestIdleCallback或分片执行、列表用虚拟滚动、给递归加明确的终止条件。提示判断是浏览器问题还是页面问题有个简单办法开一个空白标签页对比。空白页也卡那是环境问题空白页流畅那就是你的页面把主线程拖死了。6.4 面试常问的几个点顺带把原理过一遍准备前端开发面试题的时候Vue3 相关的问法无非这么几类我按被问频率说说怎么答。diff 算法Vue3 对比的是新旧两组虚拟节点尽量复用。它做了几个优化——同层比较不跨层、用 key 建立映射、处理头尾相同节点的情况、找出最长递增子序列来减少真实 DOM 的移动。核心目标就一个用最少的 DOM 操作把视图更新到位。v-if 和 v-show 的区别v-if是真正的条件渲染切换时销毁和重建组件v-show只是切display。频繁切换用v-show条件很少变的用v-if。Vue2 和 Vue3 的差异响应式实现从defineProperty换成Proxy逻辑复用从 mixin 换成组合式函数新增了 Fragment、Teleport、Suspense性能上做了静态提升、补丁标志等编译优化。组合式 API 相比 Options API 好在哪相关逻辑能写在一起而不是散在 data、methods、computed 里逻辑可以抽成函数复用比 mixin 清晰不会有命名冲突和来源不明的变量。回答这类问题有个技巧先给结论再说为什么最后补一个自己踩过的例子。纯粹背概念很干加一个真实场景的对照面试官印象会深很多。7. 后面怎么继续往上走语法和项目跑通之后往上走大致有这么几个方向你可以按兴趣挑。想更深入框架本身可以尝试手写一个简化版实现也就是常说的 mini-vue。不用追求完整先实现一个能跑通ref加模板渲染的最小版本你会对依赖收集和派发更新这两个词有完全不一样的理解。这个过程有点烧脑但收益很大之后再去看源码会轻松很多。想拓宽写法可以了解一下在 Vue3 里用 JSX。模板语法覆盖了绝大多数场景但遇到需要高度动态渲染的逻辑JSX 会更灵活。别为了用而用先用模板把业务写顺了遇到模板表达别扭的地方再考虑。想做更深度的应用下一步就是性能优化和工程化构建产物分析、按需加载、虚拟列表、首屏指标监控、CI 自动部署。这些内容在中小项目里不一定用得上但一旦项目规模上去了会不会这些直接决定你能不能在团队里扛事。最后说一个我自己觉得最有用的习惯每学一个新 API就在当前项目里找一个真实场景用一次。看十篇教程不如自己动手改一次。我当初学watch的deep和immediate选项就是因为在做菜单权限同步时踩了坑才彻底记住的——那会儿菜单数据是异步来的组件挂载时还是空的监听没加immediate就永远不触发排查了半天才反应过来。这种坑踩过一次比看文档记十遍都牢。