Vue2到Vue3学习笔记:响应式、组件通信与迁移自查 简介这份学习笔记面向准备系统入门 Vue 2.0 与 Vue 3.0 的前端学习者也适合已有基础、需要对照尚硅谷张天禹课程查漏补缺的开发者。内容围绕 Vue 的核心知识脉络展开涵盖 Vue 简介与特点、基本用法、模板语法、数据绑定、el 与 data 的写法、MVVM 模型、Object.defineProperty 与数据代理、事件处理等模块并以章节目录形式组织便于按知识点检索和复习。资源包共 1 个 docx 文档大小约 10.38MB属于个人整理的 Word 笔记适合打印、批注或作为课程配套速查资料使用。目前已有 194 人学习说明其对入门与阶段性复盘具有一定参考价值。读者可借助这份笔记梳理 Vue 基础概念、理解常见写法和易混点并结合目录结构建立从模板语法到数据处理的完整学习路径。1. 一份 Word 笔记能顶什么用从 v-for 的 key 说起Vue2.0 到 Vue3.0 的入门到精通真正卡人的往往不是概念而是参数。视频看完觉得懂了回头写v-for的key该填什么、watch的deep要不要开、$bus到底挂在哪个钩子里还是得去搜。这份学习笔记是某线上培训机构 Vue 全程课的配套整理个人按章节抄成了 Word目录带页码从模板语法、数据代理一路排到组件化、脚手架、Vuex。它解决的不是「教你怎么学」而是「写到某一行时能立刻翻到对应的参数和注意事项」。适合三类人刚看完基础视频准备动手写的手上维护 Vue2 老项目、迟早要迁 Vue3 的带新人时需要一份能直接对照的提纲的。2. 响应式地基Object.defineProperty 的 getter 与 computed/watch 的分工这一章是整份笔记里最容易被快进、也最容易在后面翻车的部分。Vue2 的响应式不是魔法它就是在data的每个属性上装了getter和settercomputed和watch的差别也不是「都能监听」而是一个靠依赖缓存求值、一个靠回调做事。把这两件事拆开后面组件通信、$nextTick、甚至 Vue3 换成Proxy之后为什么不再需要Vue.set都能顺着推出来。2.1 数据代理vm.name到底读的是谁笔记里在「数据处理」那一节先回顾了Object.defineProperty然后才讲数据代理这个顺序是对的。新建一个 Vue 实例data返回的对象并没有被直接挂到vm上而是挂在vm._data再由Object.defineProperty把每个 key 代理到vm上。所以你写vm.name实际读的是vm._data.name你写vm.name x实际写的是vm._data.name中间那层setter才有机会通知视图更新。把这两层拆成最小代码比看十遍图有用// 第一层把一个普通属性改造成可拦截的访问器属性 function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, // 能被 for...in 和 Object.keys 枚举到 configurable: true, // 允许后续 delete 或重新 define get() { console.log([get] ${key} ${val}) return val // 真实 Vue 在这里做的是 dep.depend() 收集依赖 }, set(newVal) { if (newVal val) return // 值没变直接返回避免无意义更新 console.log([set] ${key}: ${val} - ${newVal}) val newVal // 真实 Vue 在这里做的是 dep.notify()通知 watcher 重新渲染 } }) } // 第二层数据代理把 _data 上的 key 映射到实例上 function proxy(vm, sourceKey, key) { Object.defineProperty(vm, key, { get() { return vm[sourceKey][key] }, set(v) { vm[sourceKey][key] v } }) } const data { name: A同学, age: 20 } const vm { _data: data } Object.keys(data).forEach(k { defineReactive(data, k, data[k]) // 先让 _data 具备响应能力 proxy(vm, _data, k) // 再把 _data 代理到 vm 上 }) vm.name // 触发 [get] name vm.name B同学 // 触发 [set] name这段代码里三个参数值得记住。enumerable决定这个属性会不会出现在Object.keys的结果里Vue 的$data遍历依赖它configurable决定后面还能不能重新定义Vue 内部对某些属性做过二次处理val用闭包变量保存而不是回读obj[key]是为了避免get里再触发一次get造成死循环。这里有个必须提前说清的边界Object.defineProperty只能拦截「已经存在的属性」的读写拦不住新增和删除。这就是为什么在 Vue2 里给对象加一个新字段视图不会动必须走Vue.set或this.$set。同理数组通过下标赋值arr[0] x也拦不住Vue2 的做法是把push、pop、shift、unshift、splice、sort、reverse这七个方法在数组实例的原型上重写了一遍调用时先执行原逻辑再手动通知。笔记里「Vue 监测数据改变的原理」那几页画的就是这条路Vue3 换成Proxy之后能直接拦截get/set/deleteProperty/has这两个补丁就都不需要了。提示判断某个操作会不会触发更新只问一句「这个属性的setter有没有被调用」。新增 key、改数组下标、改length三者都不经过setter所以都不响应。2.2 computed 与 watch一个求值一个做事笔记把这两个放在同一节讲是对的因为初学最常见的误用就是用watch去算一个能算出来的值。computed的本质是「带缓存的 getter」它内部维护一个dirty标记依赖没变就直接返回上次的结果依赖变了才把dirty置为真、下次读取时重新求值。watch则是「监听某个值变化后执行一段逻辑」没有缓存概念也不会返回值。选型其实只有一条判断标准结果能不能由现有数据推导出来。能推导用computed不能推导要发请求、要操作 DOM、要写localStorage、要弹提示用watch。new Vue({ el: #app, data() { return { firstName: A, lastName: 同学, keyword: } }, computed: { // 派生数据只读依赖不变就不重算 fullName() { return ${this.firstName}-${this.lastName} } }, watch: { // 副作用输入变化后发请求必须防抖否则每敲一个字打一次接口 keyword: { immediate: false, // 首次绑定是否立即执行一次默认 false deep: false, // 是否深度监听对象内部变化默认 false handler(newVal, oldVal) { clearTimeout(this._timer) this._timer setTimeout(() this.fetchSuggest(newVal), 300) } } }, methods: { fetchSuggest(kw) { if (!kw) return // 这里放真实请求 } }, beforeDestroy() { clearTimeout(this._timer) // 组件销毁前清掉定时器否则回调里访问已销毁实例 } })参数说明上immediate: true会让handler在初始化时立刻跑一次适合「进页面就按默认值拉一次数据」的场景deep: true会递归遍历被监听对象的所有属性并逐个收集依赖对象层级深的时候开销明显能不写就不写非要用可以改成监听具体路径obj.a.b。另外watch的newVal和oldVal在监听引用类型时是同一个对象引用想比较差异得自己深拷贝一份旧值这是笔记里标注了「坑」的地方。computed也可以写setter虽然用得少但在需要「显示拼接值、修改时拆回去」的场景很顺手computed: { fullName: { get() { return ${this.firstName}-${this.lastName} }, set(v) { // v-model 绑到 fullName 上时这里会收到完整的新值 const [f, l] v.split(-) this.firstName f this.lastName l } } }计算属性默认只有getter直接赋值会警告Computed property fullName was assigned to but it has no setter看到这个警告不用怀疑人生补个setter就行。2.3 一个 40 行的验证页把 getter 触发次数打出来光看概念记不住笔记里那个「hello 小实例」用的是开发版 Vue就是为了让控制台能看到提示。这里可以加一段验证代码直接把「缓存」这件事变成可见的数字div idapp p{{ fullName }}/p p{{ fullName }}/p p{{ fullName }}/p button clickfirstName B改姓/button /div script srchttps://cdn.jsdelivr.net/npm/vue2/dist/vue.js/script script let evalCount 0 new Vue({ el: #app, data: { firstName: A, lastName: 同学 }, computed: { fullName() { evalCount console.log(computed 第 ${evalCount} 次求值) return this.firstName - this.lastName } } }) // 首次渲染只打印 1 次说明三个插值共用同一个缓存结果 // 点按钮后打印第 2 次说明只有依赖变了才重算 /script代码后要说明的是computed的缓存依赖 Vue 内部的watcher.dirty标记和dep收集模板里写了三次fullName只会求值一次这是它和methods最本质的差别——methods在模板里调三次就会跑三次。script引入的是vue.js开发版带完整警告和模板编译器生产环境换成vue.min.js体积小但警告也没了所以本地调试一律用开发版。跑通这段之后再回头看watch的deep和immediate就不容易混了computed的「延迟」体现在不读不算watch的「延迟」体现在值变了才回调两者是不同维度的事。3. 组件通信的四条路props、$emit、事件总线与订阅发布怎么排笔记把组件化单独拉成一章从「模块与组件、模块化与组件化」这四个概念讲到VueComponent的内置关系再落到单文件组件。这一章的价值在于它没有停在「组件是什么」而是把父传子、子传父、兄弟通信、跨层级通信四条路按复杂度排了一次序并且给了 Todo-List 这个贯穿案例。实际项目里选哪条路看的就是两个组件之间隔了几层。3.1 非单文件组件到 VueComponent那层原型链为什么重要Vue.extend返回的不是组件实例而是一个叫VueComponent的构造函数每次写School/Vue 都会new VueComponent(options)出一个新实例。每个实例的__proto__指向VueComponent.prototype而VueComponent.prototype.__proto__又指向Vue.prototype。这条链决定了组件实例能用到$mount、$emit、$watch这些挂在Vue.prototype上的方法也决定了全局事件总线可以挂在Vue.prototype上让所有组件共享。const School Vue.extend({ name: School, template: div{{ schoolName }}/div, data() { // 组件里的 data 必须是函数返回一个新对象 return { schoolName: 某高校 } } }) const vm new Vue({ el: #root, components: { School } }) // 验证原型链 console.log(School.prototype.__proto__ Vue.prototype) // true console.log(vm.$children[0] instanceof School) // truedata必须是函数这一点笔记里加了粗。原因很简单组件是可复用的如果data直接写成对象那所有实例会共享同一个引用改 A 组件的schoolNameB 组件跟着变。写成函数每次new VueComponent()都会执行一次返回全新对象作用域天然隔离。这是血泪经验级的细节但每年都有人踩。3.2 父传子 props子传父 $emit单向数据流不能破props有两种写法数组和对象。数组写法只是声明接收对象写法能加类型、必填、默认值、自定义校验工程里一律用对象写法export default { name: StudentList, props: { list: { type: Array, required: true, default: () [] // 引用类型必须用工厂函数否则多个实例共享同一数组 }, keyword: { type: String, default: }, onSelect: { type: Function, default: null }, age: { type: Number, validator: v v 0 v 150 // 返回 false 时控制台会给警告 } }, methods: { handleClick(item) { // 不要在子组件里直接改 props父组件的数据源会跟着被改 this.$emit(select, item, Date.now()) // 第二个参数起是透传给父组件的值 } } }default是引用类型必须写成函数这是props最容易翻车的一处写成对象的话所有使用该组件的实例会共享同一份默认值。validator只在开发环境生效返回false会在控制台打警告生产构建里可以放心留着。$emit的事件名建议用中划线Vue2 里v-on监听时大小写不敏感写成驼峰在模板里容易对不上。父组件这边StudentList :liststudents :age18 selectonSelect /select后面的onSelect会收到(item, timestamp)两个参数$emit传几个就收几个。单向数据流的意义在于所有修改都发生在数据源头出了问题只需要往上游查一层不用满项目找谁改了它。3.3 全局事件总线挂载、解绑与那个必写的 $off兄弟组件通信、跨层级通信最轻量的方案是全局事件总线。原理很简单new Vue()出来的实例继承了Vue.prototype上的$on、$emit、$off把这个实例挂到Vue.prototype.$bus上所有组件都能访问到同一个对象。// main.js —— 在 beforeCreate 里挂此时实例还没初始化 data 和 methods new Vue({ el: #app, render: h h(App), beforeCreate() { Vue.prototype.$bus this // this 就是当前 vm本身没有 $on 冲突 } })发送方methods: { sendMsg() { this.$bus.$emit(update-todo, this.newTodo) } }接收方mounted() { // 回调写成箭头函数或提前绑定好 this否则里头的 this 是 undefined this.$bus.$on(update-todo, this.updateTodo) }, beforeDestroy() { // 组件销毁前必须解绑否则总线对象不会释放回调组件实例跟着泄漏 this.$bus.$off(update-todo, this.updateTodo) }$off这一行是笔记里反复强调的。不写会怎样组件被销毁后$bus上还留着指向该组件方法的引用内存回收不了如果该组件被重新创建并再次$on同名事件一次$emit会触发两次回调表现为「列表数据莫名重复」或者「弹窗弹两次」。这种问题不会报错只能凭经验定位属于典型的黑匣子故障。$off不传参数会清空该事件的所有回调传第二个参数则精确解绑某一个推荐精确解绑。3.4 Todo-List 串一遍从 $emit 到 $nextTick笔记用 Todo-List 把前面所有点串起来这个案例选得好因为它同时用到了props、$emit、v-model、$nextTick和过渡动画。其中$nextTick是最容易被忽略但最有用的一个。场景是点「添加」按钮把输入框内容推进列表然后让输入框重新获得焦点。如果你写完this.todos.unshift(newItem)紧接着就this.$refs.input.focus()会发现焦点拿不到因为此刻 DOM 还没更新。methods: { addTodo() { if (!this.title.trim()) return this.todos.unshift({ id: Date.now(), title: this.title, done: false }) this.title // DOM 更新是异步的$nextTick 把回调推到更新完成之后执行 this.$nextTick(() { this.$refs.input.focus() // 此时新节点已经渲染完毕 }) } }$nextTick的内部实现会优先用Promise.then微任务降级依次是MutationObserver、setImmediate、setTimeout。所以它的回调执行时机在「数据改了、DOM 也重建了」之后同一个 tick 内多次调用会合并进同一个队列。参数是一个回调函数不传回调则返回一个 Promise用async/await也能写代码会更直白async addTodo() { this.todos.unshift(item) await this.$nextTick() this.$refs.input.focus() }理解这一点过渡动画那一节也能顺下来v-if切换时元素从无到有enter类名是在 DOM 插入之后才添加的如果手动在切换的同一行代码里去操作classList拿到的还是旧节点。4. 脚手架与工程配置vue.config.js 里四个改了就生效的参数从「直接script引入」跨到「Vue CLI 脚手架」是这份笔记的分水岭。前半部分所有例子都能在一个 HTML 里跑完后半部分开始涉及.vue单文件组件、npm run serve、跨域代理环境一变问题就来了。这一章讲的是脚手架搭起来之后哪些文件是必须懂的以及vue.config.js里那几个参数改了会立刻生效。4.1 vue create 之后的目录结构与 render 函数vue create 项目名之后生成的目录笔记里整理成了一张职责表照着看不用猜路径职责改动的频率public/index.html页面模板只有一个#app容器构建时被注入脚本极低src/main.js入口创建实例、挂载路由和 Vuex、注册全局组件高src/App.vue根组件所有页面的容器中src/components/通用组件高src/assets/静态资源会被 webpack 处理中vue.config.js可选的构建与开发服务器配置按需babel.config.js语法转译配置极低main.js里那句render: h h(App)是新手第一个疑问为什么不能写template: App/。原因是脚手架默认引入的是vue.runtime.esm.js这是运行时版本砍掉了模板编译器template字符串没人负责编译。render函数接收一个createElement函数习惯叫h直接返回虚拟节点绕过编译步骤包体更小。如果非要写template得在vue.config.js里加runtimeCompiler: true代价是多打进去一个编译器。// main.js import Vue from vue import App from ./App.vue Vue.config.productionTip false // 关掉生产环境提示开发阶段建议开着 new Vue({ render: h h(App) // 等价于把 App 组件渲染成根节点 }).$mount(#app)$mount(#app)和el: #app等价前者写法的好处是能把挂载时机延后比如等路由守卫里做完鉴权再挂。4.2 ref 与 props一个拿实例一个传数据ref用在普通 DOM 元素上this.$refs.box拿到的是原生节点用在组件上拿到的是组件实例可以直接调它的方法、读它的data。区别在于时机mounted之后才有值created阶段是undefined。template div input refkw / StudentList reflist :liststudents / /div /template script export default { mounted() { this.$refs.kw.focus() // 原生节点直接调 DOM API this.$refs.list.clearSelection() // 组件实例调子组件 methods 里的方法 } } /script$refs本身不是响应式的别在模板里绑它做条件渲染同一个ref加在v-for里会变成数组取的时候要按索引。这些边界在笔记里都有标注。props的规则在上一章讲过这里补一条工程习惯父组件传给子组件的对象如果子组件只是展示用可以在子组件里用data接一份副本避免后续误改影响父级如果确实需要改就老老实实$emit回去别想着「反正改的是引用」。4.3 mixin、plugin、scoped三处容易被合并规则绕晕的地方mixin把可复用的选项抽出来局部混入写在mixins: [m]全局混入写Vue.mixin(m)。合并规则是重点data冲突时以组件自身为准methods、computed同名也是组件覆盖混入但生命周期钩子会合并成数组混入的先执行、组件的后执行。全局混入要慎用它会污染每一个组件实例包括第三方组件排查起来非常痛苦。// mixins/count.js export default { data() { return { count: 0 } // 组件里如果也定义了 count以组件的为准 }, methods: { increment() { this.count } }, mounted() { console.log(mixin mounted) // 先于组件的 mounted 执行 } }plugin是给 Vue 加全局能力的标准姿势本质是一个带install方法的对象Vue.use()会自动调用它// plugins/loading.js export default { install(Vue, options {}) { const defaultDuration options.duration || 1000 // 挂全局方法 Vue.prototype.$showLoading function (duration defaultDuration) { console.log(loading ${duration}ms) } // 注册全局组件、全局指令也在这里做 Vue.directive(focus, { inserted: el el.focus() }) } } // main.js import LoadingPlugin from ./plugins/loading Vue.use(LoadingPlugin, { duration: 800 }) // 第二个参数会透传给 installVue.use内部有去重同一个插件重复调用只执行一次这点比自己写注册逻辑省心。scoped样式的原理是给当前组件的元素加一个>/* Vue2 支持三种写法选一种统一即可 */ .parent ::v-deep .child-title { color: red; } .parent /deep/ .child-title { color: red; } .parent .child-title { color: red; }三种写法在预处理器下的兼容性不一样::v-deep是官方推荐Sass/Less 里都能用在 Less 中会报错。这类细节笔记里如果没写全可以在项目里统一约定一种别混着来。4.4 devServer.proxy跨域那一步别配错 target开发阶段调后端接口最常遇到的是跨域。Vue CLI 内置了 webpack-dev-server代理配置写在vue.config.js里// vue.config.js module.exports { devServer: { port: 8080, // 改端口被占用时会自动往后找写了就固定 proxy: { /api: { // 请求前缀必须和代码里 axios 的 baseURL 对得上 target: http://127.0.0.1:3000, // 后端真实地址 changeOrigin: true, // 改请求头里的 Host虚拟主机部署时必开 ws: false, // 代理 WebSocket不需要就关掉 pathRewrite: { ^/api: } // 去掉前缀后端没有 /api 这一层时用 } } } }四个参数里最容易被误解的是pathRewrite。如果后端接口是http://127.0.0.1:3000/users而前端请求写的是/api/users那就需要把^/api重写掉如果后端本身就有/api前缀那就不要写pathRewrite写了反而 404。changeOrigin: true的作用是把请求头里的Host改成target的域名后端做虚拟主机或域名校验时不开会返回 403。代理只在开发环境生效生产环境靠 Nginx 或网关转发别把proxy当成上线方案。注意改了vue.config.js必须重启开发服务器热更新不会重新加载这个文件很多人改完没反应就是漏了这一步。5. 排查清单五个让视图不更新或白屏的现场前面几章讲的是怎么用这一章讲怎么修。笔记里最有价值的部分其实是那些「警告」和「备注」它们对应的就是实际开发里最常见的故障。下面五条按现象、原因、解决拆开遇到问题时可以直接对号入座。5.1 数据改了视图不动现象this.obj.newKey 1或者this.arr[0] x之后页面上对应的内容纹丝不动控制台也没有任何报错。原因Vue2 的响应式建立在Object.defineProperty的setter之上只能拦截对象已有属性的读写和数组那七个被重写过的方法。新增属性、改下标、改length都不经过setter依赖没有被通知到。解决对象新增字段用this.$set(this.obj, newKey, 1)等价于Vue.set数组改下标用this.arr.splice(0, 1, x)或者整体替换this.arr [...]。项目里更彻底的做法是提前把所有字段在data里声明好哪怕先给null也别等运行时再加。5.2 组件间数据串了现象页面上放了两个同类型的组件改其中一个另一个的内容跟着变或者刷新一次之后某个组件的数据变成了上一次打开时的值。原因最常见的是把组件的data写成了对象而不是函数所有实例共用同一份引用。其次是props的default写成了对象或数组字面量同样被多个实例共享。第三种是把数据存到了模块作用域.vue文件script顶层里模块只求值一次实例之间自然共享。解决data一律写成data() { return { ... } }props的default对引用类型一律用工厂函数() []需要跨组件共享的状态要么走Vuex要么走事件总线别用模块级变量。5.3 事件重复触发越点越多现象某个按钮点一次回调执行两次、三次随着页面切换次数增加而累加或者弹窗刷新后自己弹了出来。原因事件总线上的$on在组件销毁时没有$off旧回调残留在$bus上组件重新创建时又注册了一遍$emit自然触发多次。原生事件绑定在window或document上但没在beforeDestroy里移除也是同一类问题。解决$bus.$on和$bus.$off必须成对出现$off传具体的回调引用而不是匿名函数否则解绑不掉addEventListener同理销毁前removeEventListener。可以养成习惯凡是写mounted里加监听立刻补一行beforeDestroy解绑中间不隔代码。5.4 v-for 的 key 用了 index输入框内容错位现象列表带输入框在第一条输入文字然后删除第一条或者在头部插入一条剩下的输入框内容整体「串行」了跟数据对不上。原因key用index时元素复用依赖的是位置而不是身份。列表变化后位置 0 的元素被复用给了原本位置 1 的数据DOM 上的用户输入不受数据驱动的那部分状态跟着一起被复用了。解决key用数据的唯一标识比如item.id。只有在「列表纯展示、不涉及任何 DOM 状态、且不会重排」时才可以用index其余场景一律用 id。这是 Vue2 和 Vue3 都适用的一条面试常问实际业务里翻车频率也最高。5.5 全局混入导致第三方组件行为异常现象引入某个 UI 组件库之后页面上的按钮、输入框行为全部变得不太对控制台偶尔出现不是你写的警告。原因项目里用了Vue.mixin()做全局混入而全局混入会影响每一个组件实例包括第三方库内部定义的组件。混入里的mounted、beforeDestroy会在库组件的生命周期里执行一旦混入逻辑里访问了不存在的属性或做了 DOM 操作就会引发连锁反应。解决优先用局部mixins确实需要全局能力就改用pluginVue.prototype只挂方法不改生命周期。已经用了全局混入的在main.js里加条件判断或者把逻辑挪进Vue.directive影响面立刻收窄。6. 从 Vue2 笔记迁到 Vue3一份可勾选的自测清单笔记的下半部分开始进 Vue3两套 API 放在一起看最容易混的是那些「看起来一样、实际行为变了」的点。我的做法是先按下面这张表过一遍能自己讲清楚再动手改代码比边改边查省时间。检查项Vue2 写法Vue3 对应做法需要注意的行为差异实例创建new Vue({ el, render })createApp(App).mount(#app)全局配置从Vue.config挪到app.config响应式补丁this.$set/Vue.set不需要Proxy直接拦截删除属性、改下标都自动响应数据定义data()返回对象setup里ref/reactiveref要用.value取值计算属性computed: { fullName() {} }const fullName computed(() ...)返回的是ref模板外读要.value监听watch: { key: { deep, immediate } }watch(source, cb, { deep, immediate })监听reactive对象要传() obj或{ deep: true }过滤器filters: { fmt }已移除改成方法调用或computed事件总线Vue.prototype.$busmitt之类的轻量库或provide/inject官方不再推荐挂原型多根节点只能单根允许 Fragment样式和ref绑定要注意v-modelvalueinputmodelValueupdate:modelValue多个v-model可以直接并列写改的时候我会先动入口把new Vue换成createApp因为这一步会连带暴露出所有挂在Vue.prototype上的东西// Vue2 入口 import Vue from vue import App from ./App.vue new Vue({ render: h h(App) }).$mount(#app) // Vue3 入口 import { createApp } from vue import App from ./App.vue const app createApp(App) app.config.globalProperties.$http http // 替代 Vue.prototype.$http app.mount(#app)globalProperties是Vue.prototype的直接替代但注意它不会自动做类型推导模板里用得到、setup里得靠getCurrentInstance()拿这种别扭感就是 Vue3 组合式 API 的门槛之一。迁移顺序上我一般先换入口、再换路由和状态管理、最后逐个组件改setup中间允许options和setup混用这样每改一批都能跑起来验证不至于一次性全改完才发现某条链路断了。还有一个容易被忽略的验证点把$set全部删掉之后跑一遍原先最依赖它的那几个页面。Proxy虽然能拦截新增和删除但如果你把响应式对象整体替换掉this.obj {...}原来那个reactive代理就失效了得用Object.assign或者重新包装。这个坑我在第一次迁移时就踩过页面看着正常点开某个二级列表才发现数据不动。从那以后我每次碰 Vue2 到 Vue3 的迁移都会先把这份自测清单从头到尾勾一遍哪怕项目只有五个组件也不跳过因为漏掉一条在后期排查的成本远高于前期核对一遍。这份 Word 笔记按原课程目录顺序整理目录和页码都保留着需要对照着把某一块补上直接按章节标题翻就行。希望帮到你。本文还有配套的精品资源点击获取