☰
Vue2到Vue3学习笔记:响应式、组件通信与迁移自查
2026/10/28 10:37:00 网站建设 项目流程

简介:这份学习笔记面向准备系统入门 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和setter;computed和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 id="app"> <p>{{ fullName }}</p> <p>{{ fullName }}</p> <p>{{ fullName }}</p> <button @click="firstName = 'B'">改姓</button> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2/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) // true

data必须是函数这一点,笔记里加了粗。原因很简单:组件是可复用的,如果data直接写成对象,那所有实例会共享同一个引用,改 A 组件的schoolName,B 组件跟着变。写成函数,每次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 :list="students" :age="18" @select="onSelect" />

@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 ref="kw" /> <StudentList ref="list" :list="students" /> </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 }) // 第二个参数会透传给 install

Vue.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,确实需要全局能力就改用plugin+Vue.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-modelvalue+inputmodelValue+update: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 笔记按原课程目录顺序整理,目录和页码都保留着,需要对照着把某一块补上,直接按章节标题翻就行。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询