Vue2到Vue3迁移实战笔记:组合式API、响应式原理与避坑指南

发布时间:2026/10/15 10:45:02
Vue2到Vue3迁移实战笔记:组合式API、响应式原理与避坑指南
简介一份基于尚硅谷张天禹老师《Vue2.0Vue3.0入门到精通》课程整理的学习笔记覆盖Vue简介、开发者工具安装、NPM方式引入、模板语法、数据绑定、el挂载器与data的两种写法、MVVM模型、数据代理、事件处理及hello小实例分析等核心内容章节顺序大致跟随课程推进适合正在学习该教程的前端入门者、中级开发者以及需要系统复习Vue2/Vue3基础的人群。资源包共1个docx文档压缩包大小10.38MB笔记为个人整理采用Word格式附有层级目录便于阅读、标注、二次编辑也可以打印后对照课程逐节复盘。目前已有192人学习下载目录结构完整可按知识点快速定位遇到生疏章节时能直接回查减少手动整理笔记的时间提升跟课和复习效率。1. 这套 Vue 学习笔记到底值不值得跟一个过来人的判断后端转前端或者刚入行前端的朋友大概率都听过这套 Vue2.0 Vue3.0 入门到精通教程。说实话Vue 相关的课程和资料多到看不过来但能同时把 Vue2 和 Vue3 放在一条时间线上讲清楚的很少。大部分教程要么死守 Vue2 的选项式 API要么上来就扔一堆 Vue3 的组合式 API 让你懵。这套教程最值钱的地方在于它把两代框架的差异当主线讲而不是各讲各的所以你看完不会出现“Vue2 会用、Vue3 看不懂别人代码”的尴尬。这篇笔记不是课程目录的复述而是我跟着学完一遍之后整理出来的实战向路径。我会告诉你每章到底在解决什么问题、哪些知识点必须动手敲、哪些可以快速跳过以及我踩过的几个坑——尤其是 Vue3 里那些文档上写了但你容易忽略的细节。适合正在跟课的人用来对照进度也适合学完但感觉知识是散的、想重新串一遍的人。2. 从 Vue2 到 Vue3 的迁移不是换 API 那么简单先看懂这条主线2.1 选项式 API 与组合式 API 的本质区别不是语法换皮很多人以为 Vue3 就是把 Vue2 的 data、methods、computed 搬进 setup 函数里换个写法而已。这个理解会害了你。选项式 API 的组织单位是“选项”data 归 data、methods 归 methods同一个功能的代码被拆散在各个选项里组合式 API 的组织单位是“功能”一个功能相关的响应式变量、计算属性、监听器和函数全部放在一起。实际写起来的感觉差异非常大。Vue2 里维护一个中大型组件你经常要在一个文件里上下翻跳改一个功能要同时动 data、methods、watch 三个地方。Vue3 的 setup 函数里可以把一个功能的逻辑完整写在一个代码块里后面要删要改直接整块处理。我一般会用这个小例子给学生讲清楚差异代码不长但很说明问题// Vue2 选项式同一个 user 功能的代码被拆散 export default { data() { return { userInfo: null, loading: false } }, created() { this.fetchUser() }, methods: { async fetchUser() { this.loading true try { const res await api.getUser() this.userInfo res.data } finally { this.loading false } } } } // Vue3 组合式同一个功能完整地放在一起 import { ref, onMounted } from vue export default { setup() { const userInfo ref(null) const loading ref(false) async function fetchUser() { loading.value true try { const res await api.getUser() userInfo.value res.data } finally { loading.value false } } onMounted(fetchUser) return { userInfo, loading, fetchUser } } }组合式 API 的逻辑说明setup 函数是组件创建前的入口ref把基本类型包装成响应式引用onMounted注册生命周期钩子。所有状态和逻辑都在 setup 内部闭包中维护返回值暴露给模板使用。参数说明ref()接收任意值返回一个带有.value属性的响应式对象模板中会自动解包所以不用写.value但在 setup 内部操作时.value是必须的——这个细节新手至少会忘三次。真正让你觉得 Vue3 好用不是你被迫放弃 data 和 methods而是你能用computed和watch自由组合数据流按业务模块拆分代码。我自己跟完这套教程的最大收获就是理解了“组合式 API 是让代码按功能内聚而不是按语法类型内聚”这一层。2.2 响应式原理的差异Object.defineProperty 到 Proxy 的升级Vue2 的响应式是基于Object.defineProperty对属性进行拦截这个方案有两个绕不开的坑对象新增属性不是响应式的、数组通过索引赋值不是响应式的。所以你才会看到 Vue2 里有Vue.set、this.$set这种补救 API。Vue3 改用 Proxy 代理整个对象新增属性、删除属性、数组索引赋值统统可以被拦截到Vue.set彻底退役。但这个升级也带来了一个新的使用习惯你拿到的代理对象和原始对象不是同一个引用嵌套解构出来的属性会丢失响应式。学习笔记里复盘这部分的时候有一份对照测验题很有用——判断下列哪个场景在 Vue3 中响应式会失效。这个对照过程让我记住了一个结论只要你别把 reactive 对象解构成普通变量再到处传大部分响应式问题都能避免。// Vue3 响应式track 和 trigger 的依赖收集与派发更新 import { reactive, effect } from vue const state reactive({ count: 0 }) // 一个简单的副作用函数实际就是 Vue 内部 effect 机制的简化 effect(() { console.log(count 变化了当前值, state.count) }) // 修改值触发更新 state.count // 控制台自动打印count 变化了当前值1这里的关键逻辑reactive返回的是 Proxy 包装后的代理对象effect首次执行时会触发代理对象的 get 操作完成依赖收集之后每次属性被 set 时代理对象的 set 拦截器会通知所有依赖重新执行。ref()和reactive()的选型逻辑我一般建议基本类型用ref、对象类型用reactive但当你需要给对象整体重新赋值时reactive比较麻烦需要用Object.assign或换成ref包一层所以我的习惯是统一用ref更省心。2.3 生命周期对照表每个钩子对应的场景和注意点Vue2 和 Vue3 生命周期名称有变化初学者往往在beforeDestroy和onBeforeUnmount之间搞混。这套教程的价值在于用场景把生命周期串起来而不是让你背表。我在笔记里整理了一张对照表配合实际场景记忆效果更好。场景Vue2 钩子Vue3 组合式写法注意点组件创建前beforeCreatesetup() 本身setup 中拿不到 this数据观测完毕createdsetup() 本身可以开始处理数据DOM 挂载前beforeMountonBeforeMount一般较少使用DOM 挂载完成mountedonMounted可以操作 DOM、发请求数据变化、DOM 更新前beforeUpdateonBeforeUpdate适合更新前读取旧值数据变化、DOM 更新后updatedonUpdated注意不要在此处修改数据会死循环组件销毁前beforeDestroyonBeforeUnmount清理定时器、解绑事件组件销毁后destroyedonUnmounted善后工作这里要特别强调setup取代了beforeCreate和created两者的位置。你在 Vue2 里写在created里的逻辑在 Vue3 中直接写在 setup 函数体顶部即可。注意 setup 里没有this所有需要的数据和方法都要自己定义并 return 出去。我踩过的坑是在 setup 里习惯性写this.xxx拿 props 或触发事件结果直接报错——Vue3 组合式写法里需要用props参数和context.emit来替代。3. 把组件通信和插槽吃透教程里最容易被低估的一章3.1 组件通信的几种方式与选型场景组件通信是 Vue 应用开发的核心。父子通信用props和emit是基础但跨层级组件通信用provide/inject还是 Vuex/Pinia就需要场景判断了。这套教程的讲解顺序是合理的先讲 props 向下传递、emit 向上通知再讲事件总线然后是 Vuex/Pinia 状态管理库。不过我要提出一个和教程不同的建议事件总线在 Vue3 里已经不推荐了因为 Vue3 移除了实例上的$on、$off方法官方推荐的替代方案是mitt这种第三方库或者直接用provide/inject加响应式变量。如果你在 Vue3 项目里还需要事件总线说明组件拆分或状态管理方案需要重新考虑。// 跨层级通信provide inject ref 的组合 // 父组件或祖先组件中提供数据 import { provide, ref } from vue import ChildComponent from ./ChildComponent.vue export default { components: { ChildComponent }, setup() { const globalMessage ref(早上好) const updateMessage (msg) { globalMessage.value msg } provide(globalMessage, globalMessage) provide(updateMessage, updateMessage) } } // 任意后代组件中注入 import { inject } from vue export default { setup() { const message inject(globalMessage) const updateMessage inject(updateMessage) return { message, updateMessage } } }这个做法的逻辑说明provide可以传入 ref 响应式引用后代组件通过inject拿到的就是同一个引用修改值会触发所有注入方的更新。参数说明provide的 key 建议用一个常量字符串避免拼写错误inject可以在第二个参数传入默认值避免父级没有提供时报错。这种方式适合层级较深且中间组件不关心数据的场景比如主题色、用户信息、权限标记。3.2 插槽的进阶使用作用域插槽是提升组件复用能力的关键很多初学者用不好作用域插槽。普通插槽是父组件往子组件里塞模板但父组件拿不到子组件内部的数据。作用域插槽让子组件可以把内部数据反向暴露给父组件的插槽模板使用。这个能力在做表格组件、下拉选项组件、数据列表组件时非常有用。教程里的例子我记得很清楚实现一个带筛选功能的商品列表组件子组件内部处理筛选逻辑但每一行的展示方式由父组件完全控制。!-- 商品列表子组件 -- template div classproduct-list div v-forproduct in filteredProducts :keyproduct.id !-- 作用域插槽把商品数据暴露出去 -- slot :productproduct :indexindex{{ product.name }}/slot /div /div /template script import { ref, computed } from vue export default { props: { products: { type: Array, required: true }, filterText: { type: String, default: } }, setup(props) { const filteredProducts computed(() { return props.products.filter(p p.name.includes(props.filterText) ) }) return { filteredProducts } } } /script !-- 父组件中使用 -- template ProductList :productsallProducts filter-text手机 template #default{ product, index } span{{ index 1 }}. {{ product.name }}/span span价格{{ product.price }}/span /template /ProductList /template这里逻辑说明子组件slot :productproduct通过属性绑定的方式把数据传给父组件插槽父组件使用v-slot{ product, index }接收。参数说明作用域插槽的命名用#default表示默认插槽如果定义了具名插槽则用#slotName。这个技术的边际价值是让数据逻辑和 UI 展示完全解耦——子组件只负责数据筛选和状态维护父组件负责长什么样一套逻辑能适配列表、卡片、表格等多种形态。3.3 动态组件与 keep-alive组件切换的缓存策略做 Tab 页签或者多步骤表单时component :is...是必会的技能。但这里有个隐藏关键点动态组件默认每次切换都会销毁重建组件内部的状态会全部丢失。用keep-alive包裹后组件实例会被缓存切换时保留状态。这个知识点听起来不难但真正用的时候容易在缓存策略上翻车。keep-alive的include和exclude属性匹配的是组件 name 属性如果你的组件没有写 name 属性缓存会不生效或者全部缓存行为很难排查。我在笔记里专门标注了这一点动态组件用component :iscurrentComponent绑定需要保留状态的组件用keep-alive包裹include传组件名字符串或正则如include[TabA, TabB]被缓存的组件会多出onActivated和onDeactivated两个生命周期钩子组件通信和插槽学完之后你会发现 Vue 的开发模式已经掌握了大半。剩下的路由和状态管理更多是工程层面的套路遇到具体场景查文档就能解决。4. Vue3 新特性的几个高频场景setup 语法糖和组合式函数4.1 script setup 语法糖少写 return 带来的代码瘦身正式项目中现在基本都写script setup它让 setup 函数体变成了顶层代码变量和函数默认暴露给模板不用写 return同时也大幅减少了样板代码。这套语法糖对新手很友好但有一个理解上的门槛它只是编译时的语法糖本质还是 setup 函数。script setup import { ref, computed } from vue import { useUserStore } from /stores/user // 直接定义响应式变量不用 return const count ref(0) const doubleCount computed(() count.value * 2) // 直接定义函数不用声明 methods function increment() { count.value } // 直接使用 store const userStore useUserStore() /script template div p{{ count }} 的两倍是 {{ doubleCount }}/p button clickincrement加一/button span{{ userStore.name }}/span /div /template逻辑说明script setup中声明的所有顶层变量、函数、import 的组件都会自动暴露给模板。参数说明任何在script setup中 import 的组件都可以直接以标签形式使用不用在components选项里注册。这个写法让组件代码体积减少了约三分之一而且因为没有了 return重构时增删变量不会那么容易漏。唯一的注意点不能用script setup和普通script混用除非你想额外定义一个普通 setup 或声明模块级变量。4.2 组合式函数把一套逻辑从组件里抽出来复用Vue3 生态里最有价值的模式之一是组合式函数Composables用命名一般为useXxx。它的本质是一个普通函数内部使用 ref、reactive、computed、watch 等组合式 API最终返回状态和方法给调用方。你可以把它类比为 React 的自定义 Hook。教程中有一个模拟项目 X 就是做一个鼠标跟踪的功能——实时显示鼠标坐标。这个例子极其适合演示组合式函数的价值它把监听器逻辑、坐标状态、清理逻辑全都封装在一个函数里任何组件需要这个能力直接调用即可。// useMousePosition.js import { ref, onMounted, onUnmounted } from vue export function useMousePosition() { const x ref(0) const y ref(0) function updatePosition(e) { x.value e.clientX y.value e.clientY } onMounted(() { window.addEventListener(mousemove, updatePosition) }) onUnmounted(() { window.removeEventListener(mousemove, updatePosition) }) return { x, y } }!-- 使用组合式函数 -- script setup import { useMousePosition } from ./composables/useMousePosition const { x, y } useMousePosition() /script template div鼠标位置{{ x }}, {{ y }}/div /template这段代码的逻辑说明useMousePosition内部创建了两个 ref 变量在onMounted中注册监听器在onUnmounted中移除监听器——这个清理动作是必须的否则组件销毁后监听器还在运行造成内存泄漏。参数说明组合式函数可以接收参数比如接收一个选择器让函数只监听某个 DOM 区域内的鼠标事件也可以返回一个包含函数的状态对象让调用方灵活解构。这个模式一旦熟练你会开始把 API 请求状态loading/error/data、分页逻辑、表单校验逻辑全部抽成组合式函数代码复用率会大幅提升。5. Vue 学习过程中的避坑手册从报错到行为异常的排查记录5.1 坑一在 setup 里拿不到 this还习惯性地用this.$emit现象写完 Vue3 组件后点击按钮触发自定义事件结果控制台报TypeError: Cannot read properties of undefined (reading emit)。排查发现代码里写的是this.$emit(update:visible, false)但 setup 函数中没有 this。原因setup 函数执行时组件实例还没有完全创建所以拿不到 this。Vue3 组合式 API 设计就是让逻辑不依赖 this。解决setup 函数的第二个参数是 context里面有emit方法。改成context.emit(update:visible, false)或解构写成({ emit }) { emit(...) }。还有一个更彻底的办法写script setup时不用手动获取 emit直接使用const emit defineEmits([update:visible])。5.2 坑二reactive 对象重新赋值后响应式丢失现象用reactive({ list: [] })定义数据请求完成后写state.list res.data页面不更新。检查数据明明已经赋值成功。原因reactive返回的是代理对象如果你给state.list整体赋了一个新数组新数组是普通对象不会自动变成响应式。只有通过索引或 API 方法修改原代理对象内部的内容才会触发更新。解决如果数据的变更形态是整体替换推荐用ref定义赋值时写list.value res.data。如果坚持用reactive可以写Object.assign(state.list, res.data)保持原引用不变。这是 Vue3 响应式里最典型的“看着像 bug 其实是用法问题”的案例。5.3 坑三v-model在 Vue3 中绑定组件时的属性名变了现象把 Vue2 的组件迁移到 Vue3v-modelvisible绑定的子组件内用props: { value }接收发现值传不进去。原因Vue3 中组件上的v-model默认会传一个名为modelValue的 prop事件名变为update:modelValue。Vue2 是value和input事件。这是破坏性变更老代码不能直接迁移。解决子组件中用props: { modelValue: Boolean }事件用emit(update:modelValue, false)。如果不想改子组件也可以用v-model:visiblevisible子组件对应接收visibleprop 并派发update:visible事件这种写法在 Vue3 中更灵活还支持多个v-model绑定不同字段。5.4 坑四watch监听不到对象内部属性的变化现象用watch监听一个 reactive 对象里的user.name修改user.name时回调不执行。单独监听整个对象时才触发。原因watch默认是浅层监听监听对象属性时不会深入内部。你需要给watch传一个 getter 函数并开启deep: true或者用watchEffect配合访问属性来隐式建立依赖。解决watch(() state.user.name, (newVal, oldVal) { ... })如果修改的属性比较深层就加{ deep: true }。注意不要滥用 deep当监听对象较大时会影响性能优先用 getter 精确指定要监听的字段。5.5 坑五setup 里声明的变量模板里渲染正常但数据变化时页面不更新现象数据更新了控制台能打印新值页面纹丝不动。排查后发现变量是用普通let声明的没有用 ref 或 reactive 包装。原因Vue3 的模板响应式依赖的是 Proxy 代理的响应式对象普通变量赋值不会触发视图更新。setup 里声明的普通变量就是普通变量没有依赖收集机制。解决需要响应式的数据用ref()或reactive()包装且在script setup中直接使用。如果数据不需要响应式比如静态常量、非展示逻辑的中间变量才用普通变量声明。这里有个实用技巧如果你想要一个非响应式但会变化的变量可以用markRaw标记或者干脆放在普通变量里避免性能浪费。6. 把笔记变成自己的排错模板一个受用很久的实践习惯整套课程跟下来我收获最大的不是某个 API 的用法而是建立了一套排错模板。以前遇到页面不更新我习惯性地从头到尾翻代码看哪里写错了现在我拿到问题先按维度筛选——是数据没有更新、还是数据更新了视图没更新、还是事件触发了但没有传出、还是父子组件通信链路断了。这套排查思路就是从课程笔记的框架里反向提炼出来的。我的具体做法是维护一份问题排查清单每次踩坑就往里加一行。格式很简单现象 → 原因 → 解决 → 涉及知识点。积累到几十条之后你会发现前端 Vue 开发的坑集中在响应式、生命周期、组件通信、插槽这几个大领域远没有想象中那么多。结合这套教程的笔记经验我给还在跟课的同学一个建议不要只把代码抄一遍就完事每章学完试着回答三个问题——这个知识点不这样设计会有什么麻烦、我在什么场景下一定会用到它、如果它报错了最可能在哪个环节。把这三个问题的答案写在笔记本上比抄十遍代码都有用。这套方法最先让我打通的是 Vue3 的响应式原理后来排查线上问题时几乎靠直觉就能定位原因这完全得益于当时的笔记习惯。希望帮到你。本文还有配套的精品资源点击获取