
1. setup是什么Vue3所有新语法的地基先说一个观察我身边从Vue2迁到Vue3的同事最容易卡住的不是API变化而是思维惯性。很多人学Vue3看了一堆ref、reactive的文档真动手写代码时还是习惯性地把所有逻辑塞进一个大的export default {}里——不是不想用新语法是没弄明白新语法为什么这么设计。1.1 为什么先搞懂setup才能看懂ref和reactivesetup是Vue3 Composition API的入口它在组件实例创建之前执行是组合式逻辑的启动器。这么说太官方我换个说法在Vue2里一个组件的代码被分到data、computed、methods、watch这些网格里每个网格放对应类型的东西到了Vue3setup把所有这些网格拆掉了让你按业务逻辑而不是按数据类型来组织代码。我第一次写Vue3时踩过一个很典型的坑在setup里用了this。Vue2里this指向组件实例所以this.xxx拿数据、this.$emit发事件都是顺手的事。但setup在组件实例创建前执行根本不存在this强行用this会直接报undefined。这不是Vue3故意刁难而是设计取舍——组件实例没创建完就不能依赖实例上的方法。setup的返回值决定了模板能访问什么。返回一个对象对象里的属性直接暴露给模板返回一个渲染函数就完全掌控视图也可以两个都要不过很少这么写。这里有个容易被忽略的点setup里声明的普通变量比如let count 0改它并不会触发视图更新。因为这个变量不是响应式的Vue根本不知道它变了。要让数据变化被检测到就必须用ref或reactive包一层。1.2 三个核心APIref、reactive、computedref用来包装基本类型和对象类型访问时要用.value取值import { ref } from vue const count ref(0) function increment() { count.value // 注意这里必须 .value }在模板里count会自动解包直接写{{ count }}就行不用写count.value。这是很多新手容易困惑的地方——同样的名字在script里和模板里写法不一样。其实规则很简单模板自动解包script里要手动.value。reactive只接受对象包括数组和Map、Set返回一个深度响应式的代理对象访问属性不用.valueimport { reactive } from vue const state reactive({ user: { name: 张三, age: 28 }, tags: [前端, Vue] }) function updateUser() { state.user.name 李四 // 直接改不需要 .value }那么问题来了什么时候用ref什么时候用reactive我的习惯是——基本类型无脑ref对象类型优先reactive但对象里有单独抽取、单独修改需求的字段时用ref。举个实际场景表格搜索条件整个对象都提交给后端用reactive很顺手但一个计数器、一个开关状态、一个字符串输入值用ref最省事。computed在Vue3里的用法和Vue2差不多只是要显式引入import { ref, computed } from vue const price ref(100) const num ref(2) const total computed(() price.value * num.value)computed在这里做了缓存只有依赖的price或num变化时才重新计算。我在实际项目里经常用computed做过滤后的列表、拼接后的文案、联动的禁用状态这些场景比在模板里写一大堆表达式要清晰得多。2. 组织代码还是组织逻辑Composition API真正的价值Vue2的Options API把代码按类型切分Vue3的组合式函数Composition API把代码按功能切分。听起来只是换了个组织方式但项目一复杂差距非常大。2.1 useXxx组合式函数的拆法我接手过一个Vue2的旧后台系统一个订单详情页面足足有两千多行。数据请求在created里定时轮询在destroyed里清理页面里6个Tab各有一堆独立逻辑但全都堆在一起。每次改一个Tab的需求都得在data、computed、methods、watch之间来回跳。用Vue3组合式函数核心思路是把一个功能的所有代码数据、计算属性、方法、监听、生命周期收进同一个函数。以订单详情页为例我通常会拆成// useOrderDetail.js import { ref, onMounted, onBeforeUnmount } from vue export function useOrderDetail(orderId) { const detail ref(null) const loading ref(false) async function fetchDetail() { loading.value true try { detail.value await api.getOrderDetail(orderId) } finally { loading.value false } } onMounted(fetchDetail) return { detail, loading, fetchDetail, refresh: fetchDetail } }页面组件里这样用const orderId route.params.id const { detail, loading, refresh } useOrderDetail(orderId)这样拆完以后订单里的商品列表、物流信息、售后记录各拆一个hook每个几百行的逻辑被收敛成了几十行而且同一个hook可以在不同页面复用——比如售后记录在订单页和用户中心都可能用到直接引入就行。2.2 组件通信在新语法下的三种正确姿势Vue3的组件通信方式和Vue2不太一样我把实际项目里最常用的三种列出来。第一种props emit显式声明// 子组件 const props defineProps{ title: string, status: done | pending }() const emit defineEmits{ (e: update, id: number): void }() function handleClick() { emit(update, props.id) }Vue3.3以后用defineProps和defineEmits的泛型写法类型提示非常舒服。以前用Vue2props类型写错了只能靠运行时报错现在写代码时就能发现问题。第二种defineExpose父组件直接调子组件方法热搜词里有人搜vue3 props赋值给data其实这类需求经常是子组件要展示一个由父组件传入的初始值、再在内部修改的场景。Vue3里推荐用defineExpose暴露子组件的内部方法父组件通过ref调用// 子组件 const innerValue ref() function setInitial(val: string) { innerValue.value val } defineExpose({ setInitial })// 父组件 const childRef ref() childRef.value?.setInitial(来自父组件的值)第三种provide / inject跨层级共享多层嵌套组件之间传值用props一层层透传是灾难。用provide在祖先组件提供任意后代用inject接收// 祖先组件 provide(userContext, { currentUser, updateUser })// 任意后代组件 const { currentUser, updateUser } inject(userContext)用inject时最好给默认值万一某层组件忘了provide也不会直接抛错。2.3 Pinia为什么替代了大部分Vuex场景Vue2时代大家用Vuex做状态管理Vue3的官方推荐变成了Pinia理由很实在更少样板代码、更好的TypeScript支持、没有mutations概念。实际项目里我用Pinia存登录用户信息、全局配置、多页面共享的数据代码量大概是Vuex写法的三分之一import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: , userInfo: null }), actions: { async login(payload) { const res await api.login(payload) this.token res.token this.userInfo res.userInfo }, logout() { this.token this.userInfo null } } })相比Vuex的state mutations getters actions四个概念Pinia把state和actions合到了一起mutations直接丢掉用起来明显更贴近业务直觉。需要说明的是Vuex本身没有消失、大型复杂项目中仍有人用它但新项目我基本都是Pinia起步团队上手速度和维护成本都更友好。3. 虚拟DOM和diff算法Vue3凭什么比Vue2快搜vue3 diff算法的人特别多既有面试需求也有想搞清楚Vue3性能提升原理的。这块我尽量用大白话讲。3.1 静态标记PatchFlag和Block TreeVue2的diff是全量对比——一个模板里哪怕只有一个动态插值重新渲染时也要把整个虚拟DOM树上的所有节点都拿来比较。Vue3不一样它在编译模板时会给动态节点打标记这个标记就是PatchFlag。div span我是静态文本/span span{{ dynamicText }}/span /div这段模板编译后只有第二个span会被标记为TEXT类型文本动态第一个span被识别为静态节点渲染更新时根本不参与diff。性能提升的本质不是某个算法优化而是需要比较的节点数量大幅减少了。Block Tree的思路也类似Vue3以模板根节点为Block把动态节点收集成一个数组更新时直接遍历这个数组跳过整棵静态子树。3.2 Vue3双端diff策略的高效之处当新旧两组动态节点需要对比时Vue3采用的diff策略比Vue2更高效。Vue2的diff是首尾双指针比较 处理乱序Vue3在此基础上增加了最长递增子序列算法来优化移动操作。我用一个简单案例说明旧序列[A, B, C, D, E] 新序列[A, C, B, E, D]Vue3会先从头开始比对共同的前缀A从尾部开始比对共同的后缀E剩下的[B, C, D]和[C, B, D]需要通过索引映射找到需要移动的节点再用最长递增子序列决定哪些节点不需要移动。结果就是移动次数最少而不是简单地把所有节点都卸载重挂。这个优化在列表数据经常拖拽排序、增删元素的场景里感知非常明显——操作列表时浏览器不会频繁卡顿。3.3 为什么Vue3的diff更快但很多小项目感觉不到这事得说句公道话。对一个小页面、几十个节点Vue2和Vue3的diff性能差异用户基本感知不到。Vue3的性能优势要在大列表、频繁更新、复杂交互的页面上才明显。所以不要因为Vue3性能好就放松警惕该用v-memo、key、shallowRef的还是得用。一个容易忽略的小点列表渲染的key不要用index。关系到diff的复用策略用index当key时数组中间插入/删除元素会导致后面的节点全部重新渲染这在Vue3里依然是个大坑。4. 从零搭建一个能上线的前端工程vite TS还是JS搜vite创建vue3项目vue3安装及环境配置vue3用ts好还是js好的提问非常多这个环节我结合实践说清楚。4.1 环境准备与项目初始化Node.js版本建议16.0以上我实际用下来Node 18/20都稳定。创建项目用vite是当前的主流方式npm create vuelatest这里注意两条一是不建议用npm create vite直接创建因为npm create vuelatest会额外帮你配置好Router、Pinia、ESLint、Prettier等常用生态省掉一堆手动集成工作二是命令行交互里会问是否启用TypeScript是否启用Vue Router等新手建议先全部选No把项目跑起来再说。创建完进入目录cd my-vue3-app npm install npm run dev开发服务器起来后浏览器打开http://localhost:5173就能看到Vue3的欢迎页。出现Windows vue3开发环境这类问题时多半是PowerShell或命令行工具对node/npm路径识别有误把C:\Program Files\nodejs\加进系统PATH重启终端就能解决。4.2 TypeScript到底选不选我的观点很直接项目超过3个页面就选TS个人学习练手可以先JS但最后一定要补TS。理由很现实——Vue3的defineProps、ref、reactive这些API和TS的配合度极高有类型约束时对象结构、接口字段、事件参数都非常清楚团队协作时能省大量这个字段是什么类型的沟通成本。以ref为例JS写法是const count ref(0)TS写法是const count refnumber(0)看似只多了一个类型参数但当你用ref保存一个复杂接口对象时Vue的模板和逻辑层都会享受到类型提示字段拼错、类型传错在编译期就暴露了。当然TS的代价是需要多学一些类型写法初期会慢一些。网上有人搜vue3用ts好还是js好我的答案就一句话从长远看TS是省时间的不是花时间的。4.3 前端和后端到底怎么连搜vue3怎么连接后端vue3 前端页面连接后端的问题特别多。前端没有连接后端数据库这种概念前端和后端的交互本质是HTTP请求最常用的方案是axios。第一步装axiosnpm install axios第二步在项目里建一个统一的请求封装模块不能每个页面都直接axios.get裸调// src/utils/request.js import axios from axios const request axios.create({ baseURL: /api, // 关键走开发代理避免跨域 timeout: 10000 }) request.interceptors.response.use( response response.data, error Promise.reject(error) ) export default request第三步解决开发环境的跨域问题。在vite.config.js里配置代理把/api开头的前端请求转发到后端真实地址// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, // 后端服务地址 changeOrigin: true } } } })这样开发时前后端可以彻底分离前端用自己的端口5173后端跑在8080前端所有请求写/api/xxx即可不会遇到浏览器跨域报错。上线部署时Nginx再做一个/api反向代理到后端服务这个链路是我在多个项目里反复验证过的通用方案。5. 高频实战坑位排查那些搜索最多的问题背后Vue3相关的搜索热词里有一批问题非常集中我挑几个典型的出来按问题现象 - 排查链路 - 解决方法的方式展开看完可以直接对应到自己的项目里。5.1 props赋值给data后不更新这是被搜烂的问题父组件把数据通过props传给子组件子组件在setup里用const xxx ref(props.xxx)接收后发现父组件更新了props子组件里的值不跟着变。原因ref(props.xxx)只在setup执行时取了一次值之后props的变化和这个ref没有任何响应式关联。这相当于给初始值拍了个快照后续变化自然感知不到。解法分两种。如果只是展示直接模板里用props.xxx别拷贝。如果子组件需要修改这个值配合watch实现props变我同步变import { ref, watch } from vue const props defineProps{ initialValue: number }() const localValue ref(props.initialValue) watch(() props.initialValue, (newVal) { localValue.value newVal })还有第三种方案父组件用v-model子组件直接defineModel()Vue 3.4连手写watch都省了。这也是Vue3新语法里我很喜欢的一个特性。5.2 路由跳转了组件内容却不渲染搜router vue3 路由跳转 组件内容渲染不显示典型场景是同一个路由组件从/list?type1切到/list?type2URL变了但页面内容没刷新。排查链路先确认路由确实变了看URL以排除路由配置问题再确认是页面整片空白还是数据没变最后检查组件是否用了缓存keep-alive用了缓存就会出现复用组件实例、不重新执行onMounted的表现。根本原因Vue的路由组件在参数变化时默认会被复用不会重新销毁重建所以onMounted只执行一次。正确的做法是监听路由参数变化import { useRoute } from vue-router import { watch } from vue const route useRoute() watch(() route.query, async (newQuery) { await fetchList(newQuery.type) }, { immediate: true })这样不管是参数变化还是首次进入都能正确拉数据。如果项目里某页确实不需要复用组件也可以在router-view上加:keyroute.fullPath强制重建但性能上不如监听参数优雅。5.3 样式细节tabs标签页样式、动态登录背景、24小时分段选择器搜vue3修改tabs标签页样式vue3 登录页面 点线动态的背景vue3 24小时分段选择如何实现都属于视觉和交互类实现问题我各给一个实操方案。tabs标签页样式Element Plus的el-tabs样式定制关键是会用:deep()穿透。因为组件样式默认有scoped属性必须穿透到子组件内部才能生效。比如把选中的tab标签文字改大改粗:deep(.el-tabs__item.is-active) { font-size: 16px; font-weight: 700; }这类样式就是不生效的问题十有八九是忘了:deep()或选择器层级不对用浏览器DevTools看实际渲染出的DOM结构再调会高效很多。登录页点线动态背景这种效果本质是canvas动画。在Vue组件里用onMounted初始化画布用requestAnimationFrame驱动粒子移动和连线在onBeforeUnmount里清理动画循环let animationId onMounted(() { const canvas canvasRef.value const ctx canvas.getContext(2d) function draw() { // 画粒子、画连线 animationId requestAnimationFrame(draw) } draw() }) onBeforeUnmount(() { cancelAnimationFrame(animationId) })关键两点一是在窗口resize时要重设canvas宽高二是页面销毁时一定要取消动画帧否则组件切走后动画还在后台跑白白占用性能。24小时分段选择器这类时间轴时间段选择组件核心是把24小时拆成96个15分钟或48个30分钟的格子用二维数组记录选中状态。用户拖拽选中一段就把它标记为选中。组件里用computed把选中格子的起止时间格式化出来提交给后端的就是一个时间段数组。自己实现不算难如果赶项目进度也可以先用现成的第三方组件顶一版后续再根据业务定制。5.4 富文本和表格差异对比插件搜前端vue3富文本表格差异对比插件这类需求常见于内容管理系统、文档协同场景。Vue3生态里可选的方向有几个纯文本差异对比用diff-match-patchGoogle出品Vue3没有官方封装但可以直接用原库富文本/HTML的对比可以用html-diff-ts它能把HTML结构差异解析成带标注的增量片段结合表格数据的对比思路是先把表格转成JSON/HTML再走富文本对比管道最后在组件里渲染成并排或行内高亮的效果。这类插件一般不用深究diff算法细节把数据格式转换好、把渲染样式设计好才是项目里真正花时间的地方。先用小demo验证输出结构是否符合预期再集成进业务页面能省很多返工时间。6. 从Vue2迁移项目时的真实注意清单搜vue2和vue3区别uniapp vue2转vue3vue3 vite 和webpack的朋友大概率是在做迁移或选型。Vue2到Vue3的差异我在项目里实际踩过的点比文档里列的要具体得多。6.1 生命周期和配置项的变化最容易踩坑Vue3里beforeDestroy改成了beforeUnmountdestroyed改成了unmounted而且这些生命周期钩子在Composition API里全部要写成onBeforeUnmount、onMounted这种带on前缀的函数。迁移时如果只复制代码没改钩子名定时器、事件监听不会清理很容易出现内存泄漏或页面关不掉的诡异问题。v-model的用法也有变化。Vue2里一个组件上的v-model绑定的是valueinput事件Vue3改成了绑定modelValueupdate:modelValue事件// Vue3子组件 defineProps{ modelValue: string }() const emit defineEmits{ (e: update:modelValue, value: string): void }()这意味着从Vue2迁过来的自定义组件value和input的约定全部要改。不提前做映射配置表单类组件大概率行为错乱。6.2 全局API的变化Vue.use和过滤器都没了Vue2里注册全局组件、插件用的是Vue.use()和Vue.component()Vue3要改成app.use()和app.component()这直接关系到vue3 引入飞书sdkvue3 引入第三方插件这类操作——很多老插件文档还在写Vue.use按新方式换成app.use即可。过滤器filter在Vue3里被彻底移除了官方建议用computed或方法替代。如果老项目里大量使用了过滤器迁移时这是个体力活要一个个改成函数调用。还有$children被移除替代方案是refprovide/inject$on、$off也被移除自定义事件场景要用第三方事件库或直接走Pinia。这些差异文档里有但只有真迁移一遍才能体会工作量。6.3 Vue2项目要不要立刻迁我的建议是如果没有硬性需求比如必须用Vue3生态、团队要统一技术栈、页面性能不堪重负Vue2项目不用急着大动干戈迁移。迁移的核心难点往往不在语法差异而在于项目里的第三方依赖是否兼容Vue3——有些老库没有Vue3版本强行迁会很痛苦。如果决定迁移按这个顺序做比较稳先升级依赖vue、vue-router、vuex/pinia、组件库再全局搜索Vue.use、value/input的v-model写法、$children、filter关键字逐项替换最后跑一遍全量回归。不要指望一次迁移成功留足排错时间。7. 快问快答面试和实际开发里最常见的几个问题关于Vue3我平时被问得多的问题统一在这节回复一下这些都是别人搜热搜时踩过的门槛。Vue3用JS还是TS上面说过了3个页面以上用TS。面试的话先把TS常用语法过一遍再回答这个问题比较有底气。Vue3项目在Edge浏览器关不掉右上角最小化按钮怎么办这个热搜词很有意思其实不是Vue3的问题而是浏览器和操作系统窗口管理的问题。排查思路先看是不是全屏相关状态管理有bug再检查有没有全局的键盘/鼠标事件监听阻止了默认行为最后确认项目代码里没有改动window对象的resize、blur等窗口事件。我实际遇到过的案例是全局document.addEventListener监听没有解绑导致事件冒泡干扰了浏览器默认行为清理掉之后就好了。vue3项目里shouhideshow/hide动画怎么做搜这个词的人应该是在做显隐过渡效果。Vue3里用Transition内置组件包裹要显隐的元素用v-if或v-show控制同时在Transition里设置enter和leave相关class就能实现淡入淡出、滑动显隐等效果Transition namefade div v-ifvisible我要动起来了/div /Transition.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; }vue3实现mini-vue值得吗如果你想把Vue3的响应式原理、渲染器、diff算法彻底搞懂自己动手写一个mini版是最快的学习路径b站上有不少开源教程可以跟。面试时能在纸上把reactive基于Proxy的实现框架画出来、把effect依赖收集讲清楚绝对是个加分项。vue3商城项目练手怎么做商城的核心不是UI而是状态管理 权限 接口对接这些工程化能力。用Vue3 Vite Pinia Element Plus或NutUI搭一个包含商品列表、购物车、订单、登录注册、后台上架流程的后台管理系统是性价比很高的练手项目。搜vue3后台管理系统vue3商城能找到大量参考但建议自己实现核心逻辑不要直接clone教程源码改个皮。8. 最后分享几个我日常开发中的小习惯这些习惯不是标准答案但确实帮我减少了很多返工。第一所有跟后端对接的请求统一走封装的request实例不要裸用axios。拦截器里统一处理token过期、错误提示和loading状态后端接口字段一改只用动一处封装不用几百个页面逐个改。第二组件内的数据流转尽量遵循父组件props进入 - 子组件内部处理 - 通过emit回传的单向流动。如果一个子组件要修改父组件的数据优先用v-model或defineModel而不是直接操作props引用。第三在写任何组件前先回答三个问题这个组件的输入是什么、输出是什么、变更由谁触发。把这三个问题想清楚再动手组合式函数和props/emit的设计基本不会跑偏。最后开发环境多花10分钟做工程化配置性能翻倍不止。比如Vite的路径别名指向src路由和store用懒加载动态导入这些配置写过一次之后新页面开发完全不用再想这个文件放哪的问题整个团队的开发体验都会上一个台阶。