
简介这是一份面向 Vue 初学者的系统学习笔记围绕尚硅谷张天禹 Vue2.0 与 Vue3.0 入门到精通课程整理而成适合正在跟课、需要课后复习或准备前端面试的开发者使用。内容以 Word 笔记形式组织涵盖 Vue 简介、基本用法、模板语法、数据绑定、MVVM 模型、数据代理与事件处理等核心知识点目录层级清晰便于按章节检索和对照视频查漏补缺。压缩包内共 1 个 docx 文件约 10.38MB单文件即可完整查阅省去多文档切换的麻烦。已有 194 人学习说明其对跟课复习具有一定参考价值。读者可借助这份整理稿快速回顾 Vue2 与 Vue3 的基础概念、常用写法与知识脉络也可在练习前用作提纲提升学习与复盘效率。1. Vue2.0 Vue3.0 入门到精通卡住人的从来不是知识点数量“Vue2.0 Vue3.0 入门到精通”这条路线最坑的地方不是内容多而是顺序。不少人把 Vue2 的选项式 API 背到能默写生命周期打开 Vue3 文档却发现 setup 里连 this 都没有攒下的手感一夜清零。真实情况是两版的模板语法、指令、组件注册重合度很高差异集中在响应式实现、组件逻辑组织、生态工具这三处。把这三处单拎出来对照着学比按版本线性推进省一半时间。这篇笔记写给已经能写基础 HTML/CSS、看得懂 ES6 解构和箭头函数、准备把 Vue 当主力框架的人按“同一需求写两遍 → 看底层为什么不同 → 生态怎么换 → 哪里会翻车”推进每段都给能直接跑的代码。2. 选项式 API 与组合式 API把同一个待办列表写两遍再对比同一个需求写两遍是最快建立差异感的方式。选待办列表是因为它同时覆盖了表单双向绑定、列表渲染、计算属性、方法、本地状态五件事两版的写法差异在这一个页面上全能暴露。2.1 先划清边界哪些代码可以原样搬哪些必须重写模板层几乎是通用的。v-if、v-for、v-bind、v-on、v-model在两边语义一致:key的规则也一样Vue3 只是把v-for上的key放到了更严格的编译检查里。指令修饰符.prevent、.stop、.once通用.native在 Vue3 被移除因为 inheritAttrs 的默认行为改了。组件注册差异不大全局注册从Vue.component()换成app.component()挂载从new Vue({ el: #app })换成createApp(App).mount(#app)本质是把“全局单例”改成了“应用实例”一个页面挂多个 Vue 应用不再互相污染。必须重写的是组件内部逻辑的组织方式。Vue2 把数据放data、方法放methods、监听放watch、计算放computed一个功能的代码被切成四份Vue3 的 setup 允许把同一个功能的数据、方法、监听写在一起逻辑聚合的粒度从“选项类型”变成“业务功能”。这个差别在小组件里看不出好处一旦组件超过 300 行Vue2 里改一个功能要在文件里上下跳五六次Vue3 里就是连续的一段。生态层要单独算账$on/$off/$once实例方法被移除事件总线要换第三方库过滤器filters被移除改用计算属性或方法$children被移除改用模板 ref。2.2 两套写法实现同一个待办列表先看 Vue2 版本这是选项式的典型形态// TodoList.vue - Vue2 选项式写法 export default { name: TodoList, data() { return { keyword: , // 输入框内容 list: [ { id: 1, text: 读响应式原理, done: false }, { id: 2, text: 写一个 Pinia store, done: true } ] } }, computed: { // 依赖 keyword 和 list任一变化都会重算 filtered() { const k this.keyword.trim().toLowerCase() if (!k) return this.list return this.list.filter(item item.text.toLowerCase().includes(k)) }, doneCount() { return this.list.filter(item item.done).length } }, methods: { add() { const text this.keyword.trim() if (!text) return // Vue2 中直接 push 能被拦截因为数组方法被重写过 this.list.push({ id: Date.now(), text, done: false }) this.keyword }, toggle(item) { // 改的是对象已存在的属性能触发更新 item.done !item.done } } }再看不带语法糖的 Vue3 setup 写法重点看 this 的消失和 ref 的存在感// TodoList.vue - Vue3 组合式写法setup 函数形式 import { ref, computed } from vue export default { name: TodoList, setup() { // ref 包装基本类型和数组模板中自动解包JS 里必须 .value const keyword ref() const list ref([ { id: 1, text: 读响应式原理, done: false }, { id: 2, text: 写一个 Pinia store, done: true } ]) const filtered computed(() { const k keyword.value.trim().toLowerCase() if (!k) return list.value return list.value.filter(item item.text.toLowerCase().includes(k)) }) const doneCount computed(() list.value.filter(i i.done).length) function add() { const text keyword.value.trim() if (!text) return list.value.push({ id: Date.now(), text, done: false }) keyword.value } function toggle(item) { item.done !item.done } // 必须返回模板才能访问返回渲染函数则不返回对象 return { keyword, list, filtered, doneCount, add, toggle } } }script setup语法糖把这层 return 也省了script setup import { ref, computed } from vue const keyword ref() const list ref([]) const filtered computed(() { const k keyword.value.trim().toLowerCase() return k ? list.value.filter(i i.text.toLowerCase().includes(k)) : list.value }) function add() { const text keyword.value.trim() if (!text) return list.value.push({ id: Date.now(), text, done: false }) keyword.value } // 顶层声明的变量、函数自动暴露给模板无需 return /script三段代码的逻辑完全一致差别在三个地方。第一Vue2 的this指向组件实例所有状态挂在实例上Vue3 的 setup 里this是undefined状态是局部变量靠 return 或语法糖暴露。第二Vue2 的data必须是函数返回对象这是为了组件复用时状态隔离Vue3 用ref天然隔离不存在共享问题。第三Vue2 的computed是一个对象键是属性名Vue3 的computed是一个函数调用返回只读 ref要写计算属性得传{ get, set }。ref的.value是新手最容易漏的在script里读写必须带.value在template里由编译器自动解包所以模板里写keyword就够了。漏写.value的典型症状是赋值不生效且不报错因为你在给 ref 对象本身重新赋值响应式系统看到的是一次普通属性覆盖。2.3 三个必调参数setup 的执行时机、this 的替代、ref 的拆箱规则setup 在beforeCreate之前执行此时组件实例还没创建完成this不可用props和context是仅有的两个入参。props是响应式的直接解构会丢响应性要解构得用toRefsimport { toRefs } from vue export default { props: { title: String, count: { type: Number, default: 0 } }, setup(props) { const { title } toRefs(props) // 保留响应性的解构 // 或者用 computed 包一层const title computed(() props.title) return { title } } }第二个参数context包含attrs、slots、emit、expose。Vue2 里this.$emit发事件Vue3 的 setup 里用context.emitscript setup里用defineEmits声明。attrs在 Vue3 包含了原来$attrs和$listeners两部分因为$listeners被合并进来了这意味着v-bind$attrs会把事件监听一起透传下去写高阶组件时要注意别把不该透传的监听带出去。ref 的拆箱规则值得单列。ref在模板顶层自动解包但嵌在对象或数组里不会自动解包const obj { a: ref(1) }模板里访问obj.a得到的是 ref 对象而不是数字。reactive包裹的对象里的 ref 会自动解包const state reactive({ a: ref(1) })state.a就是 1。这条不对称规则是很多“数据明明是响应式却不更新”的根源。reactive的限制也要记住只能接收对象类型传基本类型会警告且不生效解构会丢失响应性const { count } reactive({ count: 0 })之后count只是一个普通数字重新赋值整个对象也不行state reactive({})会切断原来的代理引用。需要整体替换的场景用ref需要解构的场景用toRefs。3. 响应式底层从 defineProperty 换到 Proxy调试时该盯哪一行知道底层怎么实现不是为了面试是为了在数据不更新时能三秒定位而不是逐个排查。两版响应式的差异直接决定了一批“看起来该更新却没更新”的写法Vue2 里踩过的坑Vue3 里可能不存在Vue3 里的新坑Vue2 里根本不会出现。3.1 Vue2 里数组和新增属性为什么“改了没反应”Vue2 用Object.defineProperty把 data 里的每个属性改写成 getter/settergetter 里收集依赖setter 里派发更新。这个方案有两个先天缺陷。一是无法监听属性的新增和删除。defineProperty只能在初始化时对已存在的 key 逐个定义obj.newKey 1走的是一次普通赋值setter 根本不存在。解决办法是Vue.set(obj, newKey, 1)或this.$set它内部手动调用defineProperty再通知依赖。删除同理要用Vue.delete。二是无法监听数组索引赋值和length修改。arr[0] 1和arr.length 0都不会触发更新因为 Vue2 没有拦截数字索引。它的做法是把push、pop、shift、unshift、splice、sort、reverse七个方法重写在原方法执行后手动派发更新。所以arr.push()有效arr[0] x无效。现场复现一次就懂了// Vue2 组件内 export default { data() { return { user: { name: A同学 }, list: [a, b, c] } }, mounted() { this.user.age 18 // 视图不更新age 不是响应式的 this.$set(this.user, age, 18) // 有效 this.list[0] z // 视图不更新 this.list.splice(0, 1, z) // 有效splice 被重写过 this.list.length 0 // 视图不更新 this.list [] // 有效整体替换会走 setter } }排查口诀改的是不是初始化时就存在的属性如果是新增的用$set改的是不是数组索引或 length如果是换splice或整体替换。这条口诀在 Vue2 项目里能解决大部分“数据变了页面没变”。3.2 手写 40 行 Proxy 响应式验证依赖收集到底在做什么Vue3 换成Proxy代理的是整个对象get 拦截读、set 拦截写新增、删除、索引赋值全部在拦截范围内前面三类问题一次性消失。删掉Vue.set不是偷懒是真的不需要了。手写一个最小版本比看十遍文章都直观// mini-reactive.js let activeEffect null // 当前正在执行的效果函数 const targetMap new WeakMap() // target - key - Seteffect function track(target, key) { if (!activeEffect) return let depsMap targetMap.get(target) if (!depsMap) { depsMap new Map() targetMap.set(target, depsMap) } let dep depsMap.get(key) if (!dep) { dep new Set() depsMap.set(key, dep) } dep.add(activeEffect) // 收集这个 key 被谁依赖了 } function trigger(target, key) { const depsMap targetMap.get(target) if (!depsMap) return const dep depsMap.get(key) if (dep) dep.forEach(effect effect()) // 派发通知所有依赖重跑 } function reactive(target) { return new Proxy(target, { get(obj, key, receiver) { track(obj, key) return Reflect.get(obj, key, receiver) }, set(obj, key, value, receiver) { const old obj[key] const ok Reflect.set(obj, key, value, receiver) if (old ! value) trigger(obj, key) // 值真变了才触发 return ok }, deleteProperty(obj, key) { const ok Reflect.deleteProperty(obj, key) trigger(obj, key) // 删除也触发Vue2 做不到 return ok } }) } function effect(fn) { activeEffect fn fn() // 首次执行顺带完成依赖收集 activeEffect null }跑一段验证const state reactive({ count: 0, nested: { a: 1 } }) effect(() { console.log(count 变化了, state.count) }) state.count // 打印“count 变化了1” state.count 5 // 打印“count 变化了5” delete state.count // 打印“count 变化了undefined”Vue2 里删属性不触发 state.nested.a 2 // 不打印因为 nested 没被代理递归处理最后一行暴露了关键差异Vue3 是惰性代理只有访问到nested时才把返回值套一层 Proxy上例中state.nested返回的是原始对象所以a的修改不被追踪。真实 Vue3 里reactive的 get 拦截会对返回值做判断是对象就再包一层这就是“惰性递归”的含义。这也解释了一个坑把 reactive 对象整体传给子组件没问题但把它深拷贝出去再改副本是没有代理的。trigger里的old ! value判断也有讲究。Vue2 的 setter 里同样比较新老值值没变就不派发避免无意义的渲染。手写时漏掉这行会出现“赋了相同值也重渲染”的浪费。3.3 ref 与 reactive 的选型规则和三个拆箱边界选型上我一般按这个顺序判断需要整体替换、需要用基本类型、需要解构返回的一律ref一个纯对象状态且不会整体替换的用reactive。ref是万能的reactive是有边界的反过来不成立。项目里统一用ref加.value虽然啰嗦但迁移成本最低团队协作时不用记两套心智模型。三个拆箱边界要背下来。第一模板顶层自动解包{{ count }}写的是 ref 名模板里不要写.value写了反而报错。第二reactive对象内部的 ref 属性会自动解包state.count直接是值但state.list[0]如果是 ref 则不会解包数组下标访问是例外。第三函数传参不解包foo(countRef)传进去的是 ref 对象函数内部要自己处理.value这也是组合式函数返回 ref 时调用方必须.value的原因。调试时有个黑匣子技巧在控制台打印一个变量如果输出是{ value: 1 }而页面显示1说明模板解包生效了但 JS 层还是 ref如果输出是Proxy { count: 0 }说明是 reactive 对象如果输出是原始对象没有 Proxy 包装那它一定不是响应式的改它当然不更新。这一条能把“响应式失效”和“数据没赋上”两类问题区分开。依赖收集的时机也要注意。computed和watch都是惰性的computed在被访问时才求值并收集依赖watch默认在数据变化后才触发回调watchEffect则立即执行一次并自动收集依赖。三者的执行顺序在同一个 tick 里是watch 的 pre 回调 → 组件更新 → watch 的 post 回调配flush: post能让回调拿到更新后的 DOM需要读取渲染结果时这个是必调参数。4. 组件通信与工程化迁移从 $on 到 mitt、从 vue-cli 到 Vite 的对照清单组件通信是两版差异最密集的区域因为 Vue3 把几个实例方法直接删了。工程化这块更容易卡人vue-cli 的项目结构、环境变量、代理配置和 Vite 完全不同照着 Vue2 的教程在 Vite 项目里操作十有八九在启动那一步就报错。4.1 props/emit 与 v-model 的默认值差异父传子两边一样都是 props声明方式有差别Vue2 是props: [title]或props: { title: String }Vue3 的script setup用编译宏defineProps它不需要 import且声明的 props 类型在编译期就能提示。script setup // defineProps 是编译宏不用 import不能赋值给变量后再当作 props 用 const props defineProps({ title: { type: String, required: true }, list: { type: Array, default: () [] } // 对象/数组默认值必须是工厂函数 }) const emit defineEmits([update:title, change]) // defineEmits 返回 emit 函数也可以不接收返回值直接调用 defineEmits([change]) /script子传父的差异更大。Vue2 用this.$emit(change, payload)Vue3 用emit(change, payload)第一个参数不是事件名而是自定义事件名没有$前缀。$emit在 Vue3 实例上仍然存在但不建议在 setup 里用拿不到类型提示。v-model的变化是重灾区。Vue2 中组件上的v-model默认绑定valueprop 和input事件要改绑其他 prop 得用model选项。Vue3 默认改成modelValueprop 和update:modelValue事件而且支持多个v-model// Vue3 子组件支持 v-model 和 v-model:visible const props defineProps({ modelValue: String, visible: Boolean }) const emit defineEmits([update:modelValue, update:visible]) function onInput(e) { emit(update:modelValue, e.target.value) // 事件名必须是 update:xxx }父组件写Child v-modelmsg v-model:visibleshow /等价于传modelValue和visible两个 prop监听update:modelValue和update:visible两个事件。把 Vue2 项目里的.sync迁移过来时v-bind:title.synct要改成v-model:titlet事件名从update:title保持不变这是少数改名后语义更统一的点。defineExpose是另一处必须注意的script setup组件默认是封闭的父组件通过模板 ref 拿到的实例只能访问defineExpose暴露出去的东西。Vue2 里父组件this.$refs.child.someMethod()直接就能调Vue3 里不 expose 就是 undefined这个坑在表单组件封装时最常见。4.2 跨层级通信provide/inject 与 Pinia 的最小骨架祖孙组件传值Vue2 用provide/inject或者事件总线。事件总线在 Vue2 里靠Vue.prototype.$bus new Vue()加上$on/$emitVue3 把$on、$off、$once从实例上删掉了常见做法是换mitt// bus.js import mitt from mitt export const bus mitt() // 发送方 import { bus } from ./bus bus.emit(user-login, { id: 1 }) // 接收方注意组件卸载时解绑否则内存泄漏 import { onUnmounted } from vue import { bus } from ./bus const handler payload console.log(payload) bus.on(user-login, handler) onUnmounted(() bus.off(user-login, handler))mitt的on和off必须传同一个函数引用用箭头函数内联注册就解绑不掉这是事件总线在 Vue3 里最常见的泄漏源。能用 provide/inject 或 Pinia 解决的我一般不用总线因为它的数据流向不可追踪。provide/inject两版写法差不多Vue3 要注意 provide 的值如果是响应式的注入方拿到的仍是响应式引用但 provide 一个基本类型则不会更新// 祖先组件 import { provide, ref, readonly } from vue const theme ref(dark) function toggleTheme() { theme.value theme.value dark ? light : dark } provide(theme, { theme: readonly(theme), toggleTheme }) // 用 readonly 防止后代直接改 // 后代组件 import { inject } from vue const { theme, toggleTheme } inject(theme, { theme: ref(light), toggleTheme: () {} })第二个参数是默认值祖孙层级深的时候一定要给否则 inject 返回 undefined报错位置会离真正原因很远。状态管理从 Vuex 到 Pinia 是 Vue3 项目的标准动作。Pinia 去掉了 mutationsactions 同时承担同步和异步模块化从嵌套 modules 变成多个独立 store 文件// stores/user.js import { defineStore } from pinia import { ref, computed } from vue export const useUserStore defineStore(user, () { const token ref() const profile ref({ name: }) const isLogin computed(() !!token.value) async function login(payload) { const res await fetch(/api/login, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }) const data await res.json() token.value data.token profile.value data.user } function logout() { token.value profile.value { name: } } return { token, profile, isLogin, login, logout } })这种 setup 风格的 store 和组件写法一致返回值就是对外暴露的 API内部可以随意用 computed 和 watch。defineStore的第一个参数是 store id必须全局唯一重复会直接报错。组件里const userStore useUserStore()之后userStore.token直接可读写不需要mapState、mapActions那一套映射这也是迁移时删代码最多的地方。4.3 路由与构建的三处硬改createRouter、history 模式、VITE_ 变量Vue Router 3 到 4 的改动集中在创建方式和通配路由。new VueRouter({ mode: history, routes })换成createRouter({ history: createWebHistory(), routes })mode: hash对应createWebHashHistory()。通配路由path: *在 v4 不再支持必须写成path: /:pathMatch(.*)*否则启动就报错。导航守卫的签名也变了next()仍然可用但官方推荐返回值风格// router/index.js import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), // BASE_URL 来自 Vite routes: [ { path: /, component: () import(../views/Home.vue) }, // 路由懒加载 { path: /user/:id, component: () import(../views/User.vue), props: true }, { path: /:pathMatch(.*)*, component: () import(../views/NotFound.vue) } ] }) router.beforeEach((to) { // 返回 false 取消导航返回路由地址则重定向不写 return 即放行 if (to.meta.requiresAuth !localStorage.getItem(token)) { return { path: /login, query: { redirect: to.fullPath } } } })props: true把路由参数解耦成组件 props组件不用useRoute()也能拿到 id测试时更好写。这个选项在 Vue2 时代就有但很多人不用迁移时顺手加上能省掉一批this.$route的改动。构建工具从 vue-cli 换到 Vite三处最容易翻车。第一是环境变量前缀VUE_APP_改成VITE_而且读取方式从process.env.VUE_APP_API改成import.meta.env.VITE_APIprocess在 Vite 的浏览器环境里根本不存在直接报未定义。第二是路径别名Vite 需要手动配// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) // 不要用 __dirnameESM 下没有 } }, server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) // 按后端实际路径决定要不要剥前缀 } } } })第三是静态资源引用require(/assets/logo.png)在 Vite 里不工作要么用new URL(..., import.meta.url).href要么直接import logo from /assets/logo.png。public目录下的文件用绝对路径/xxx.png引用这条规则两版通用。5. 避坑排查5 个让学习进度直接停摆的现场这一节按“现象 → 原因 → 解决”写都是我在跟着课程写 demo 和自己改项目时反复遇到的卡住的时间从半小时到一整天不等。5.1 现象改了数据页面纹丝不动控制台还不报错原因分三种。最常见的是把reactive对象整体替换了let state reactive({ list: [] })之后state reactive({ list: [1] })模板里绑的还是旧代理新对象和视图没关系。第二种是解构丢响应性const { count } state之后改count只在改局部变量。第三种是给ref赋值忘了.value在 script 里写count 5覆盖的是变量本身。解决整体替换场景改用ref写state.value {...}需要解构就套toRefs(state)养成 script 里见到 ref 先想.value的习惯。判断依据很简单在控制台打印变量看到Proxy才是响应式对象看到原始对象就说明链路断了。5.2 现象父组件调子组件方法报is not a function原因子组件用了script setup但没有defineExpose。script setup默认封闭组件实例父组件通过 ref 拿到的只是一个空壳代理除了少量内置属性什么都访问不到。解决在子组件显式暴露script setup import { ref } from vue const inputRef ref(null) function focus() { inputRef.value?.focus() } defineExpose({ focus }) // 只暴露这个方法内部状态仍然封闭 /script顺带一个注意点父组件在onMounted里访问子组件 ref 是安全的但在 setup 顶层直接访问会拿到 null因为子组件还没挂载。需要更早的时机就用nextTick或者放到onMounted里。5.3 现象Vue3 项目里this.$router是 undefined原因在script setup或 setup 函数里用了this。Vue3 的 setup 中this是undefinedthis.xxx全部报错另一种情况是选项式 API 和组合式混写时把methods里的写法搬到了 setup 里。解决路由用useRouter()和useRoute()import { useRouter, useRoute } from vue-router const router useRouter() // 用于 push/replace/back const route useRoute() // 用于读取 params/query/meta router.push({ name: user, params: { id: 1 } })注意useRoute返回的是响应式对象route.params.id在组件内会随路由变化更新但解构出来就丢了响应性要解构用toRefs(route)。这两个函数必须在 setup 同步执行阶段调用放到setTimeout里会拿不到当前实例。5.4 现象Vite 项目里process.env.xxx报未定义原因Vite 不注入 Node 的process全局变量vue-cli 那套VUE_APP_前缀和process.env读取方式整体失效。有人试图在vite.config.js里define一个process.env出来能跑但会把构建期常量写死切换环境时容易出问题。解决环境变量文件改成.env.development、.env.production变量名用VITE_前缀代码里统一import.meta.env.VITE_API_BASE。只有VITE_开头的变量才会暴露给客户端其他前缀的变量在浏览器里读不到这条安全边界不要绕过。需要类型提示就在src/env.d.ts里补ImportMetaEnv接口声明。5.5 现象从 Vue2 抄过来的过滤器写法直接编译失败原因Vue3 移除了filters选项和|管道语法模板里写{{ price | currency }}会编译报错。解决改成计算属性或方法。格式化逻辑复用多的抽成普通函数放utils// utils/format.js export function currency(value, symbol ¥) { if (value null || isNaN(value)) return - return symbol Number(value).toFixed(2) } // 组件内 import { currency } from /utils/format // 模板{{ currency(price) }} 或包一层 computed用方法而不是计算属性时要注意方法在每次重渲染都会执行纯格式化且依赖不变的场景性能差别可以忽略但涉及大数组遍历的格式化还是放computed里缓存更稳。6. 一张迁移自查表确认自己真的从“能看懂”走到“能改”学完两套 API 不等于能上手改项目。我自己的判断标准是拿到一个 Vue2 组件能在不查文档的情况下改写成script setup并且改完行为一致。这个标准比“看完了多少节课”靠谱得多。下面这张表是我每次动手前会过一遍的清单按组件层、状态层、工程层分三块每行都对应一个具体的检查动作。检查项Vue2 写法Vue3 对应写法验证方式组件入口new Vue({ el })createApp(App).mount()页面能挂载多个 app 互不干扰状态声明data() { return {} }ref/reactive控制台打印是 Proxy 或 ref 对象计算属性computed: {}computed(() ...)依赖变化后值同步更新侦听器watch: {}watch(source, cb)深度监听需显式deep: true生命周期mountedonMounted同名钩子只执行一次子组件调用this.$refs.child.init()defineExpose 模板 ref父组件能拿到方法且不报错双向绑定valueinputmodelValueupdate:modelValue多 v-model 可同时生效事件总线$on/$emitmitt卸载时off掉同一函数引用全局状态VuexmutationsPiniaactions异步 action 能正常改 state路由创建new VueRoutercreateRouter通配路由用/:pathMatch(.*)*环境变量process.env.VUE_APP_*import.meta.env.VITE_*生产构建产物里值正确替换表里最容易被跳过的是“验证方式”那一列。很多人改完代码页面能跑就认为迁移完成了结果某个deep: true没加、某个通配路由没改上线才发现 404。逐项验证看着慢实际比线上回滚快得多。具体技巧上我一般用“小切口改造”而不是整体重写先挑一个纯展示、没有状态的组件改成script setup跑通后再改带 props 和 emit 的最后动带路由和 store 的。每改完一个用git diff看一遍改动范围如果某次 diff 里混进了业务逻辑修改说明这次改大了回退拆小。这个习惯救过我好几次尤其是选项式里的this依赖散落在 methods、computed、watch 三处时一次性重写很容易漏掉某个this.$nextTick。另一个血泪经验是别在迁移过程中同时升级依赖。Vue2 升 Vue3、UI 库跟着升、路由和状态管理一起换出问题时根本分不清是哪一层导致的。我现在的顺序是先升 Vue 本体用兼容构建跑一遍确认组件都能渲染再换路由再换状态管理最后换构建工具。每一步单独提交、单独验证回滚成本最低。至于“后悔药”就是每个阶段开始前打一个 tag真出问题直接切回去比翻提交记录快。最后说一句掏心窝的Vue2 到 Vue3 的迁移难点从来不是新 API 的语法而是旧代码里那些“能跑就行”的隐性依赖——$children遍历、$listeners透传、原型链上挂的全局方法。这些在文档里都是一行“已移除”在真实项目里可能牵扯十几个文件。把这张表当成体检清单改一项勾一项比追求“一次改对”现实得多。希望帮到你。本文还有配套的精品资源点击获取