Vue 2 与 Vue 3 语法区别及使用技巧
面向刚接触 Vue、需要阅读 Vue 2 老项目或开始编写 Vue 3 项目的开发者。
本文主要比较 Vue 2.6 的经典写法与 Vue 3 的现代写法。Vue 2.7 已内置部分 Composition API,Vue 3 也仍然支持 Options API,遇到不同版本时请先查看项目的
package.json。
快速导航
- 建立整体认识:第 1 至 4 节。
- 掌握常用语法:第 5 至 16 节。
- 了解高级差异:第 17 至 25 节。
- 学习迁移与实战:第 26 至 31 节。
- 日常查询:第 32 至 34 节。
1. 先用一句话理解区别
Vue 2 和 Vue 3 的共同目标都是:用数据驱动页面,并把页面拆成可复用组件。
它们最大的区别不是模板完全变了,而是 Vue 3 提供了更灵活的代码组织方式、更完整的 TypeScript 支持和更高效的响应式系统。
text
Vue 2 常见写法:Options API
把 data、methods、computed、watch 按"选项类型"分开放置
Vue 3 推荐写法:Composition API + <script setup>
把同一个业务功能的状态、计算、监听和方法放在一起
例如,一个"用户搜索"功能包含:
- 搜索关键词。
- 搜索结果。
- 加载状态。
- 请求方法。
- 关键词监听。
Vue 2 通常会把这些内容分散到 data、methods 和 watch 中;Vue 3 可以把它们集中写在同一段代码中,还能进一步抽成 useUserSearch() 复用。
2. 不要混淆三个概念
2.1 Vue 版本不等于 API 风格
| 说法 | 是否正确 | 原因 |
|---|---|---|
| Vue 2 只能使用 Options API | 不完全正确 | Vue 2.7 内置 Composition API,旧版本也可安装插件 |
| Vue 3 只能使用 Composition API | 错误 | Vue 3 完整支持 Options API |
Vue 3 推荐 <script setup> |
正确 | 它更简洁,类型推导也更自然 |
| Options API 已被废弃 | 错误 | Vue 3 官方仍然支持 |
Vue 3 Options API 示例:
vue
<script>
export default {
data() {
return {
count: 0,
}
},
methods: {
increment() {
this.count += 1
},
},
}
</script>
这段代码可以在 Vue 3 中正常使用。
2.2 <script setup> 是 Vue 3 的编译语法糖
以下两种 Vue 3 写法表达相似的意思。
普通 setup():
vue
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
count.value += 1
}
return {
count,
increment,
}
},
}
</script>
<script setup>:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
/**
* 将计数器增加 1。
*
* @returns 无返回值
*/
function increment() {
count.value += 1
}
</script>
<script setup> 中的顶层变量和函数可以直接在模板中使用,不需要手动 return。
2.3 Vue、构建工具和应用框架不是一回事
| 名称 | 作用 | 常见版本组合 |
|---|---|---|
| Vue | 页面组件和响应式框架 | Vue 2、Vue 3 |
| Vue CLI | 基于 Webpack 的项目脚手架 | Vue 2 项目中常见 |
| Vite | 现代开发与构建工具 | Vue 3 项目中常见,也支持 Vue 2 |
| Nuxt | 基于 Vue 的应用框架 | Nuxt 2 常配 Vue 2,Nuxt 3 常配 Vue 3 |
| Vue Router | 前端路由 | Vue Router 3 常配 Vue 2,4 常配 Vue 3 |
| Vuex / Pinia | 状态管理 | Vue 2 常见 Vuex,Vue 3 推荐 Pinia |
所以,"Vue 2 升级 Vue 3"经常还会同时涉及 Router、Vuex、UI 组件库和构建工具升级。
3. 如何确认项目使用哪个版本
先查看 package.json:
json
{
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.0.0",
"pinia": "^2.0.0"
}
}
常见判断依据:
| 特征 | 更可能是 Vue 2 | 更可能是 Vue 3 |
|---|---|---|
vue 版本 |
2.x |
3.x |
| 应用入口 | new Vue() |
createApp() |
| 路由版本 | Vue Router 3 | Vue Router 4 |
| 状态管理 | Vuex 3 / 4 | Pinia 或 Vuex 4 |
| 组件脚本 | export default {} |
<script setup> |
| 响应式 API | data() |
ref()、reactive() |
不要只凭文件长相判断版本。Vue 3 也可以写 export default {},Vue 2.7 也可能出现 ref()。
在运行时也可以查看:
js
import { version } from 'vue'
console.log(version)
4. 核心语法总览
| 场景 | Vue 2 经典写法 | Vue 3 推荐写法 |
|---|---|---|
| 创建应用 | new Vue() |
createApp() |
| 组件状态 | data() |
ref()、reactive() |
| 方法 | methods |
普通函数 |
| 计算属性 | computed 选项 |
computed() |
| 监听 | watch 选项 |
watch()、watchEffect() |
| 生命周期 | mounted |
onMounted() |
| Props | props 选项 |
defineProps() |
| 事件声明 | 通常不声明 | defineEmits() |
| 双向绑定 | value + input |
modelValue + update:modelValue |
| 逻辑复用 | Mixins | Composables |
| 全局配置 | Vue.use()、Vue.component() |
app.use()、app.component() |
| 根节点 | 必须单根节点 | 支持多个根节点 Fragment |
| TypeScript | 支持有限 | 原生设计重点 |
模板中的大部分基础语法没有变化:
vue
<template>
<section>
<input v-model.trim="keyword">
<button :disabled="loading" @click="search">
搜索
</button>
<p v-if="loading">加载中...</p>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</section>
</template>
{``{ }}、v-if、v-for、v-bind、v-on 和大多数修饰符在两个版本中都很相似。
5. 单文件组件结构
5.1 Vue 2 常见结构
vue
<script>
export default {
name: 'UserCounter',
props: {
initialCount: {
type: Number,
default: 0,
},
},
data() {
return {
count: this.initialCount,
}
},
computed: {
doubledCount() {
return this.count * 2
},
},
methods: {
increment() {
this.count += 1
},
},
}
</script>
<template>
<section>
<p>当前:{{ count }}</p>
<p>两倍:{{ doubledCount }}</p>
<button @click="increment">增加</button>
</section>
</template>
<style scoped>
button {
color: #1677ff;
}
</style>
5.2 Vue 3 推荐结构
vue
<script setup lang="ts">
import { computed, ref } from 'vue'
interface Props {
initialCount?: number
}
const props = withDefaults(defineProps<Props>(), {
initialCount: 0,
})
const count = ref(props.initialCount)
const doubledCount = computed(() => count.value * 2)
/**
* 将当前计数增加 1。
*
* @returns 无返回值
*/
function increment() {
count.value += 1
}
</script>
<template>
<section>
<p>当前:{{ count }}</p>
<p>两倍:{{ doubledCount }}</p>
<button @click="increment">
增加
</button>
</section>
</template>
<style scoped>
button {
color: #1677ff;
}
</style>
主要变化:
- 不再需要
export default。 - 不再按
data、computed、methods分类。 - 不需要使用
this。 <script setup>顶层声明可直接供模板使用。- TypeScript 类型可以直接服务于 Props 和事件。
6. 创建应用与挂载方式
6.1 Vue 2
js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
6.2 Vue 3
js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
Vue 3 把全局操作限制在某个应用实例上。一个页面创建多个 Vue 应用时,它们的插件、组件和配置不会互相污染。
使用技巧:
createApp()之后先注册插件和全局能力,最后调用mount()。app.mount()返回根组件实例,但业务代码不要依赖它操控内部组件。- Vue 3 挂载后会保留挂载容器,组件内容渲染到容器内部;这与 Vue 2 替换挂载元素的行为不同。
7. 数据与响应式状态
7.1 Vue 2 使用 data()
vue
<script>
export default {
data() {
return {
count: 0,
user: {
name: '张三',
},
users: [],
}
},
}
</script>
组件的 data 必须是函数,以保证每个组件实例得到独立数据。
访问和修改时使用 this:
js
this.count += 1
this.user.name = '李四'
7.2 Vue 3 使用 ref()
ref() 适合数字、字符串、布尔值、数组、对象,以及需要整体替换的状态。
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
const keyword = ref('')
const users = ref([])
count.value += 1
users.value = [{ id: 1, name: '张三' }]
</script>
使用技巧:
- 在
<script>中读写ref必须使用.value。 - 在
<template>中 Vue 会自动解包,直接写count,不要写count.value。 - 团队不确定选什么时,优先
ref(),规则更统一,整体替换也方便。
vue
<template>
<p>{{ count }}</p>
<input v-model="keyword">
</template>
7.3 Vue 3 使用 reactive()
reactive() 适合结构稳定、经常修改内部字段的对象。
vue
<script setup>
import { reactive } from 'vue'
const form = reactive({
name: '',
age: 18,
enabled: true,
})
form.name = '张三'
form.age += 1
</script>
reactive() 的主要限制:
- 只能接收对象类型,不能直接接收数字或字符串。
- 不要随意把整个响应式对象替换为普通对象。
- 直接解构会丢失字段与源对象的响应式连接。
错误示例:
js
let form = reactive({ name: '' })
// form 变量现在指向普通对象,原代理失去连接
form = { name: '张三' }
需要整体替换时可以使用 ref():
js
const form = ref({ name: '' })
form.value = { name: '张三' }
7.4 reactive 解构技巧
下面的写法会丢失响应式连接:
js
const state = reactive({ count: 0 })
const { count } = state
state.count += 1
console.log(count) // 仍然是解构时的普通数字
需要解构时使用 toRefs():
js
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
keyword: '',
})
const { count, keyword } = toRefs(state)
7.5 响应式原理差异
| 项目 | Vue 2 | Vue 3 |
|---|---|---|
| 核心实现 | Object.defineProperty |
Proxy |
| 新增对象属性 | 默认无法检测,需要 $set |
可以直接检测 |
| 删除对象属性 | 需要 $delete |
可以直接检测 |
| 数组索引赋值 | 默认无法检测 | 可以直接检测 |
| 数组长度修改 | 默认无法检测 | 可以直接检测 |
Map / Set |
支持有限 | 可响应式代理 |
Vue 2:
js
this.$set(this.user, 'age', 18)
this.$delete(this.user, 'age')
this.$set(this.users, 0, newUser)
Vue 3:
js
user.age = 18
delete user.age
users[0] = newUser
注意:Vue 3 的 Proxy 不能代理 IE 11,因此 Vue 3 不支持 IE 11。
8. 方法与 this
8.1 Vue 2 使用 methods
vue
<script>
export default {
data() {
return {
count: 0,
}
},
methods: {
increment(step = 1) {
this.count += step
},
},
}
</script>
Vue 2 的 methods 不要使用箭头函数:
js
methods: {
// 错误:箭头函数没有自己的 this
increment: () => {
this.count += 1
},
}
8.2 Vue 3 使用普通函数
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
/**
* 按指定步长增加计数。
*
* @param {number} step 每次增加的数值,默认值为 1
* @returns {void} 无返回值
*/
function increment(step = 1) {
count.value += step
}
</script>
没有 this 后,变量来源更明确:
count是当前文件声明的状态。increment是当前文件声明的方法。- 导入的工具函数可以从
import直接看出来源。
使用技巧:业务方法优先使用有名称的函数声明,调试堆栈更清晰;数组回调、短计算可以使用箭头函数。
9. 计算属性
计算属性适合"根据已有状态得到新值"。它会缓存结果,依赖不变化时不会重复计算。
9.1 Vue 2
vue
<script>
export default {
data() {
return {
firstName: '三',
lastName: '张',
}
},
computed: {
fullName() {
return `${this.lastName}${this.firstName}`
},
},
}
</script>
9.2 Vue 3
vue
<script setup>
import { computed, ref } from 'vue'
const firstName = ref('三')
const lastName = ref('张')
const fullName = computed(() => {
return `${lastName.value}${firstName.value}`
})
</script>
9.3 可写计算属性
Vue 2:
js
computed: {
fullName: {
get() {
return `${this.lastName}${this.firstName}`
},
set(value) {
const [lastName, firstName] = value.split(' ')
this.lastName = lastName
this.firstName = firstName
},
},
}
Vue 3:
js
const fullName = computed({
get() {
return `${lastName.value} ${firstName.value}`
},
set(value) {
const [newLastName, newFirstName] = value.split(' ')
lastName.value = newLastName
firstName.value = newFirstName
},
})
使用技巧:
- 能用计算属性表达的派生值,不要再存一份状态。
- 计算函数中避免请求接口、写入状态等副作用。
- 需要执行副作用时使用
watch()。
10. 监听状态
10.1 Vue 2 watch
vue
<script>
export default {
data() {
return {
keyword: '',
form: {
name: '',
},
}
},
watch: {
keyword(newValue, oldValue) {
console.log('关键词变化', newValue, oldValue)
},
form: {
deep: true,
immediate: true,
handler(newForm) {
console.log('表单变化', newForm)
},
},
},
}
</script>
10.2 Vue 3 watch()
vue
<script setup>
import { reactive, ref, watch } from 'vue'
const keyword = ref('')
const form = reactive({
name: '',
})
watch(keyword, (newValue, oldValue) => {
console.log('关键词变化', newValue, oldValue)
})
watch(
form,
(newForm) => {
console.log('表单变化', newForm)
},
{
deep: true,
immediate: true,
},
)
</script>
监听 reactive 对象时本身就是深层监听,但明确写出 deep 有时更便于表达意图。监听某个字段时必须传入 getter:
js
watch(
() => form.name,
(newName) => {
console.log('姓名变化', newName)
},
)
错误写法:
js
// 传入的是当前字符串,不是响应式数据源
watch(form.name, () => {})
10.3 Vue 3 watchEffect()
watchEffect() 会自动收集回调中用到的响应式依赖,并立即运行一次。
js
import { ref, watchEffect } from 'vue'
const keyword = ref('')
const page = ref(1)
watchEffect(() => {
console.log('查询条件', keyword.value, page.value)
})
选择技巧:
| 需求 | 推荐方式 |
|---|---|
| 明确监听一个或多个值 | watch() |
| 需要新值和旧值 | watch() |
| 需要懒执行 | watch() |
| 依赖较多且希望自动收集 | watchEffect() |
| 根据状态产生同步新值 | computed() |
异步搜索还要处理竞态或取消旧请求,不能只依靠监听器:
js
watch(keyword, async (newKeyword, _oldKeyword, onCleanup) => {
const controller = new AbortController()
onCleanup(() => {
controller.abort()
})
await fetch(`/api/users?keyword=${encodeURIComponent(newKeyword)}`, {
signal: controller.signal,
})
})
11. 生命周期
11.1 名称对照
| 业务阶段 | Vue 2 Options API | Vue 3 Composition API |
|---|---|---|
| 实例创建前 | beforeCreate |
通常直接写在 setup 顶层 |
| 实例创建后 | created |
通常直接写在 setup 顶层 |
| 挂载前 | beforeMount |
onBeforeMount |
| 挂载后 | mounted |
onMounted |
| 更新前 | beforeUpdate |
onBeforeUpdate |
| 更新后 | updated |
onUpdated |
| 卸载前 | beforeDestroy |
onBeforeUnmount |
| 卸载后 | destroyed |
onUnmounted |
| 缓存组件激活 | activated |
onActivated |
| 缓存组件停用 | deactivated |
onDeactivated |
| 捕获后代错误 | errorCaptured |
onErrorCaptured |
Vue 3 Options API 中,销毁钩子也改名为 beforeUnmount 和 unmounted。
11.2 Vue 2 示例
vue
<script>
export default {
mounted() {
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
},
methods: {
handleResize() {
console.log(window.innerWidth)
},
},
}
</script>
11.3 Vue 3 示例
vue
<script setup>
import { onBeforeUnmount, onMounted } from 'vue'
/**
* 输出当前浏览器窗口宽度。
*
* @returns {void} 无返回值
*/
function handleResize() {
console.log(window.innerWidth)
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
</script>
使用技巧:
- 请求首屏数据可以放在
onMounted();如果框架支持服务端渲染,应优先使用框架的数据获取 API。 - 定时器、事件监听、观察器和第三方实例要在卸载时清理。
- 不要在
onUpdated()中无条件修改状态,容易造成无限更新。
12. Props:父组件向子组件传数据
12.1 Vue 2
vue
<script>
export default {
props: {
title: {
type: String,
required: true,
},
count: {
type: Number,
default: 0,
},
user: {
type: Object,
default() {
return {}
},
},
},
}
</script>
对象和数组默认值必须由函数返回,避免多个实例共享同一个对象。
12.2 Vue 3 JavaScript 写法
vue
<script setup>
const props = defineProps({
title: {
type: String,
required: true,
},
count: {
type: Number,
default: 0,
},
})
console.log(props.title)
</script>
12.3 Vue 3 TypeScript 写法
vue
<script setup lang="ts">
interface User {
id: number
name: string
}
interface Props {
title: string
count?: number
user?: User
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
user: () => ({
id: 0,
name: '',
}),
})
</script>
核心规则在两个版本中一致:Props 是只读输入,子组件不要直接修改。
错误示例:
js
props.count += 1
props.user.name = '李四'
虽然嵌套对象的修改有时不会立即报错,但它仍然修改了父组件拥有的数据,数据流会变得难以追踪。
正确做法:
- 只需要通知父组件:发送事件。
- 需要本地编辑:复制成表单副本,保存时发送新值。
- 需要受控双向状态:使用
v-model约定。
13. Emit:子组件通知父组件
13.1 Vue 2
子组件:
vue
<script>
export default {
methods: {
submit() {
this.$emit('save', {
id: 1,
name: '张三',
})
},
},
}
</script>
父组件:
vue
<UserEditor @save="handleSave" />
Vue 2 中可以不声明事件,直接调用 $emit()。
13.2 Vue 3
子组件:
vue
<script setup lang="ts">
interface User {
id: number
name: string
}
const emit = defineEmits<{
save: [user: User]
cancel: []
}>()
/**
* 将编辑完成的用户数据提交给父组件。
*
* @returns 无返回值
*/
function submit() {
emit('save', {
id: 1,
name: '张三',
})
}
</script>
父组件写法仍然是:
vue
<UserEditor @save="handleSave" @cancel="handleCancel" />
Vue 3 建议显式声明组件会发出的事件,优点是:
- 事件名称和参数更清楚。
- TypeScript 能检查参数。
- 声明过的监听器不会作为普通属性落到组件根元素上。
组件事件不会像原生 DOM 事件那样自动向所有祖先冒泡,只会到直接父组件。
14. v-model 的重要变化
14.1 原生表单上的基本用法
两版都可以写:
vue
<input v-model.trim="name">
<input v-model.number="age" type="number">
<input v-model="enabled" type="checkbox">
常用修饰符:
| 修饰符 | 作用 |
|---|---|
.trim |
去掉输入值两端空格 |
.number |
尝试把输入值转换为数字 |
.lazy |
从 input 时机改为 change 时机同步 |
14.2 Vue 2 自定义组件 v-model
Vue 2 默认使用:
text
Prop:value
事件:input
父组件:
vue
<NameInput v-model="name" />
子组件:
vue
<script>
export default {
props: {
value: {
type: String,
default: '',
},
},
methods: {
handleInput(event) {
this.$emit('input', event.target.value)
},
},
}
</script>
<template>
<input :value="value" @input="handleInput">
</template>
也可以通过 model 选项改名:
js
export default {
model: {
prop: 'checked',
event: 'change',
},
props: {
checked: Boolean,
},
}
14.3 Vue 3 自定义组件 v-model
Vue 3 默认使用:
text
Prop:modelValue
事件:update:modelValue
父组件:
vue
<NameInput v-model="name" />
子组件:
vue
<script setup lang="ts">
defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
/**
* 将输入框的新值同步给父组件。
*
* @param event 原生输入事件
* @returns 无返回值
*/
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
</script>
<template>
<input :value="modelValue" @input="handleInput">
</template>
在支持 defineModel() 的 Vue 3 版本中可以进一步简化:
vue
<script setup lang="ts">
const model = defineModel<string>({ required: true })
</script>
<template>
<input v-model="model">
</template>
14.4 Vue 3 支持多个 v-model
父组件:
vue
<UserName
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
子组件:
vue
<script setup lang="ts">
defineProps<{
firstName: string
lastName: string
}>()
defineEmits<{
'update:firstName': [value: string]
'update:lastName': [value: string]
}>()
</script>
Vue 2 的 .sync:
vue
<UserDialog :visible.sync="dialogVisible" />
Vue 3 应迁移为具名 v-model:
vue
<UserDialog v-model:visible="dialogVisible" />
对应子组件事件仍然是 update:visible。
15. 模板语法的重要差异
15.1 v-if 与 v-for 优先级
同时写在一个元素上时:
- Vue 2:
v-for优先。 - Vue 3:
v-if优先。
下面这段代码在 Vue 3 中会因为 user 尚未进入 v-for 作用域而出错:
vue
<li v-for="user in users" v-if="user.enabled" :key="user.id">
{{ user.name }}
</li>
两个版本都不推荐混写。使用计算属性过滤:
js
const enabledUsers = computed(() => {
return users.value.filter(user => user.enabled)
})
vue
<li v-for="user in enabledUsers" :key="user.id">
{{ user.name }}
</li>
或者明确增加模板层级:
vue
<template v-for="user in users" :key="user.id">
<li v-if="user.enabled">
{{ user.name }}
</li>
</template>
15.2 v-for 的 key
Vue 3 在 <template v-for> 上使用 key:
vue
<template v-for="user in users" :key="user.id">
<UserCard :user="user" />
<UserActions :user-id="user.id" />
</template>
使用技巧:
- 优先使用稳定的业务 ID。
- 会增删、排序或包含表单状态的列表,不要使用数组下标作为
key。 - 条件分支不再需要故意使用相同
key复用元素;Vue 3 会为分支生成唯一 key。
15.3 事件按键码被移除
Vue 2 可能出现:
vue
<input @keyup.13="submit">
Vue 3 使用按键名称:
vue
<input @keyup.enter="submit">
15.4 .native 修饰符被移除
Vue 2 为组件根元素监听原生事件:
vue
<MyButton @click.native="handleClick" />
Vue 3 移除了 .native。未在子组件 emits 中声明的监听器会作为透传属性交给根元素:
vue
<MyButton @click="handleClick" />
如果子组件声明了 click 事件,就应由子组件显式 emit('click')。
15.5 v-bind 合并顺序变化
Vue 3 按书写顺序决定同名属性最终值:
vue
<!-- id 最终是 explicit-id -->
<div v-bind="attrs" id="explicit-id" />
<!-- id 最终可能来自 attrs -->
<div id="explicit-id" v-bind="attrs" />
迁移旧代码时,不要依赖模糊的覆盖规则,明确安排属性顺序。
16. 插槽 Slot
16.1 Vue 2 常见写法
子组件:
vue
<section>
<header>
<slot name="header" />
</header>
<main>
<slot :user="user" />
</main>
</section>
父组件旧语法:
vue
<UserPanel>
<template slot="header">
用户信息
</template>
<template slot-scope="slotProps">
{{ slotProps.user.name }}
</template>
</UserPanel>
Vue 2.6 已引入 v-slot,推荐写法与 Vue 3 接近:
vue
<UserPanel>
<template #header>
用户信息
</template>
<template #default="{ user }">
{{ user.name }}
</template>
</UserPanel>
16.2 Vue 3
Vue 3 统一使用 v-slot 或 # 简写:
vue
<UserPanel>
<template #header>
用户信息
</template>
<template #default="{ user }">
{{ user.name }}
</template>
</UserPanel>
在脚本中访问插槽:
vue
<script setup>
const slots = defineSlots()
console.log(Boolean(slots.header))
</script>
普通 setup() 中可以使用 useSlots() 或 setup 上下文的 slots。
17. 根节点与 Fragment
Vue 2 组件模板必须只有一个根节点:
vue
<template>
<div>
<header>标题</header>
<main>内容</main>
</div>
</template>
Vue 3 支持多个根节点:
vue
<template>
<header>标题</header>
<main>内容</main>
</template>
但多根节点会影响属性自动透传。父组件写:
vue
<PageLayout class="compact" />
如果 PageLayout 有多个根节点,Vue 不知道 class 应放到哪个节点,需要手动绑定 $attrs:
vue
<template>
<header>标题</header>
<main v-bind="$attrs">内容</main>
</template>
vue
<script setup>
defineOptions({
inheritAttrs: false,
})
</script>
使用技巧:多根节点能减少无意义容器,但不要为了少写一个 div 破坏页面语义或属性归属。
18. $attrs 与 $listeners
Vue 2:
$attrs包含未被 Props 接收的普通属性,不包含class和style。$listeners包含父组件传入的事件监听器。
vue
<input v-bind="$attrs" v-on="$listeners">
Vue 3:
$listeners被移除并合并到$attrs。class和style也包含在$attrs中。- 事件会表现为
onClick、onInput等属性。
vue
<input v-bind="$attrs">
在 <script setup> 中可以使用:
js
import { useAttrs } from 'vue'
const attrs = useAttrs()
attrs 适合透传,不适合作为主要响应式业务状态。需要稳定使用的输入应声明为 Props。
19. 模板引用与组件方法暴露
19.1 Vue 2
父组件:
vue
<template>
<UserEditor ref="editor" />
</template>
<script>
export default {
mounted() {
this.$refs.editor.reset()
},
}
</script>
父组件通常可以访问子组件实例上的大部分内容,耦合较强。
19.2 Vue 3
子组件显式暴露方法:
vue
<script setup>
import { reactive } from 'vue'
const form = reactive({
name: '',
})
/**
* 重置当前编辑表单。
*
* @returns {void} 无返回值
*/
function reset() {
form.name = ''
}
defineExpose({
reset,
})
</script>
父组件:
vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import UserEditor from './UserEditor.vue'
interface UserEditorExpose {
reset: () => void
}
const editorRef = ref<UserEditorExpose | null>(null)
onMounted(() => {
editorRef.value?.reset()
})
</script>
<template>
<UserEditor ref="editorRef" />
</template>
使用技巧:
- 优先使用 Props 和 Emit 描述数据流。
- 只在聚焦输入框、重置表单、调用第三方组件实例等命令式场景使用模板引用。
- 调用前处理
null,组件挂载前引用还不存在。
20. 依赖注入 provide/inject
20.1 Vue 2
祖先组件:
js
export default {
provide() {
return {
theme: this.theme,
}
},
}
后代组件:
js
export default {
inject: ['theme'],
}
Vue 2 中需要特别注意注入值的响应式行为。
20.2 Vue 3
祖先组件:
vue
<script setup lang="ts">
import { provide, readonly, ref } from 'vue'
const theme = ref<'light' | 'dark'>('light')
/**
* 切换页面主题。
*
* @returns 无返回值
*/
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('theme', {
theme: readonly(theme),
toggleTheme,
})
</script>
后代组件:
vue
<script setup>
import { inject } from 'vue'
const themeContext = inject('theme')
</script>
大型 TypeScript 项目建议使用 InjectionKey,避免字符串冲突并获得类型推导。
使用技巧:
- 适合当前组件树内跨很多层共享的上下文,如表单组、主题、局部注册器。
- 全局业务数据优先使用 Pinia。
- 状态修改方法最好由提供者一起提供,后代不要随意直接改共享状态。
21. 逻辑复用:Mixins 与 Composables
21.1 Vue 2 Mixins
js
export const paginationMixin = {
data() {
return {
page: 1,
pageSize: 20,
}
},
methods: {
resetPage() {
this.page = 1
},
},
}
js
import { paginationMixin } from './paginationMixin'
export default {
mixins: [paginationMixin],
}
Mixins 的主要问题:
- 在组件中看不出某个字段来自哪个 Mixin。
- 多个 Mixin 可能出现同名冲突。
- 依赖关系隐藏在
this上。 - TypeScript 类型推导困难。
21.2 Vue 3 Composables
ts
import { ref } from 'vue'
/**
* 创建分页状态及其操作方法。
*
* @param initialPageSize 初始每页条数
* @returns 分页状态和重置方法
*/
export function usePagination(initialPageSize = 20) {
const page = ref(1)
const pageSize = ref(initialPageSize)
/**
* 将当前页重置为第一页。
*
* @returns 无返回值
*/
function resetPage() {
page.value = 1
}
return {
page,
pageSize,
resetPage,
}
}
组件中使用:
vue
<script setup>
import { usePagination } from './usePagination'
const {
page,
pageSize,
resetPage,
} = usePagination(50)
</script>
Composables 的优势:
- 导入来源明确。
- 可以自由重命名返回值。
- 可以传入参数。
- 可以组合其他 Composable。
- 更容易进行类型检查和单元测试。
命名技巧:组合式函数通常以 use 开头,如 usePagination()、useUserSearch()。
22. Filter 过滤器被移除
Vue 2:
vue
<p>{{ price | currency }}</p>
js
export default {
filters: {
currency(value) {
return `¥${Number(value).toFixed(2)}`
},
},
}
Vue 3 移除了模板过滤器语法。改用普通函数或计算属性:
vue
<script setup>
/**
* 将数值格式化为人民币金额文本。
*
* @param {number|string} value 待格式化金额
* @returns {string} 人民币金额文本
*/
function formatCurrency(value) {
return `¥${Number(value).toFixed(2)}`
}
</script>
<template>
<p>{{ formatCurrency(price) }}</p>
</template>
使用技巧:
- 纯格式化逻辑抽到
utils,便于多个组件复用和测试。 - 格式化成本高或依赖多个状态时使用
computed()。 - 日期、货币等优先使用
Intl.DateTimeFormat、Intl.NumberFormat或项目统一工具。
23. 事件总线变化
Vue 2 有实例事件 API,因此一些项目会这样创建事件总线:
js
const eventBus = new Vue()
eventBus.$on('user-updated', handleUserUpdated)
eventBus.$emit('user-updated', user)
eventBus.$off('user-updated', handleUserUpdated)
Vue 3 移除了组件实例上的 $on、$off 和 $once。组件仍可使用 $emit 通知父组件,但不能再把 Vue 实例当通用事件总线。
替代方案:
| 场景 | 推荐方式 |
|---|---|
| 直接父子组件 | Props + Emit |
| 多层局部组件树 | provide/inject |
| 跨页面业务状态 | Pinia |
| 少量无状态事件 | mitt 等轻量事件库 |
不要默认用事件总线传业务数据,它会隐藏事件来源和接收者。使用外部事件库时,组件卸载前必须移除监听。
24. 全局 API 与插件
24.1 Vue 2
js
import Vue from 'vue'
import App from './App.vue'
import MyPlugin from './plugins/myPlugin'
import BaseButton from './components/BaseButton.vue'
Vue.use(MyPlugin)
Vue.component('BaseButton', BaseButton)
Vue.directive('focus', focusDirective)
Vue.prototype.$api = apiClient
new Vue({
render: h => h(App),
}).$mount('#app')
24.2 Vue 3
js
import { createApp } from 'vue'
import App from './App.vue'
import MyPlugin from './plugins/myPlugin'
import BaseButton from './components/BaseButton.vue'
const app = createApp(App)
app.use(MyPlugin)
app.component('BaseButton', BaseButton)
app.directive('focus', focusDirective)
app.config.globalProperties.$api = apiClient
app.mount('#app')
对应关系:
| Vue 2 | Vue 3 |
|---|---|
Vue.use() |
app.use() |
Vue.component() |
app.component() |
Vue.directive() |
app.directive() |
Vue.mixin() |
app.mixin() |
Vue.config.* |
app.config.* |
Vue.prototype.$xxx |
app.config.globalProperties.$xxx |
Vue 3 Composition API 中,不建议大量依赖 globalProperties。普通服务可以通过模块导入,组件树上下文可以通过插件配合 provide/inject。
Vue 3 插件结构:
ts
import type { App, InjectionKey } from 'vue'
interface ApiClient {
get: (url: string) => Promise<unknown>
}
export const apiClientKey: InjectionKey<ApiClient> = Symbol('apiClient')
export const apiPlugin = {
/**
* 向 Vue 应用安装 API 客户端。
*
* @param app 当前 Vue 应用实例
* @param apiClient 需要注入的 API 客户端
* @returns 无返回值
*/
install(app: App, apiClient: ApiClient) {
app.provide(apiClientKey, apiClient)
},
}
25. 自定义指令钩子变化
常见钩子对照:
| Vue 2 | Vue 3 | 说明 |
|---|---|---|
bind |
created / beforeMount |
指令绑定阶段 |
inserted |
mounted |
元素插入页面后 |
update |
移除 | 使用其他更新钩子 |
componentUpdated |
updated |
所在组件更新后 |
unbind |
unmounted |
元素卸载后 |
Vue 3 聚焦指令:
ts
import type { Directive } from 'vue'
export const focusDirective: Directive<HTMLInputElement> = {
/**
* 元素挂载后自动获得焦点。
*
* @param element 应获得焦点的输入元素
* @returns 无返回值
*/
mounted(element) {
element.focus()
},
}
使用:
vue
<input v-focus>
优先用组件、Props 和普通模板表达 UI;只有需要直接操作底层 DOM 生命周期时才使用自定义指令。
26. Render 函数变化
一般业务页面优先写 <template>,只有高度动态的组件库封装才需要 Render 函数。
26.1 Vue 2
js
export default {
props: ['level'],
render(h) {
return h(`h${this.level}`, this.$slots.default)
},
}
26.2 Vue 3
h 不再作为参数传入,需要从 Vue 导入:
js
import { h } from 'vue'
export default {
props: {
level: {
type: Number,
required: true,
},
},
setup(props, { slots }) {
return () => h(`h${props.level}`, null, slots.default?.())
},
}
Vue 3 的 VNode Props 更扁平:
js
h('button', {
class: 'primary_button',
disabled: false,
onClick: handleClick,
}, '保存')
从 Vue 2 迁移 JSX、Render 函数或复杂组件库时,需要单独检查 VNode 数据结构和插槽调用方式。
27. 新增内置能力
27.1 Teleport
把组件内容渲染到当前 DOM 层级之外,常用于弹窗、抽屉和全局提示:
vue
<Teleport to="body">
<div v-if="visible" class="dialog_mask">
<section class="dialog">弹窗内容</section>
</div>
</Teleport>
组件逻辑关系没有改变,Props、Emit 和注入仍按原组件树工作。
27.2 Suspense
可为异步依赖提供等待状态:
vue
<Suspense>
<AsyncUserPanel />
<template #fallback>
<p>加载中...</p>
</template>
</Suspense>
使用前要确认当前 Vue 版本和框架对 Suspense 的支持范围。
27.3 多根节点
Vue 3 的 Fragment 允许组件返回多个根节点,已在第 17 节说明。
28. 状态管理与路由生态
这些不是 Vue 核心语法,但升级项目时经常一起变化。
28.1 Vuex 与 Pinia
Vue 2 常见 Vuex:
js
export default new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count += 1
},
},
})
Vue 3 推荐 Pinia:
ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
/**
* 将全局计数增加 1。
*
* @returns 无返回值
*/
function increment() {
count.value += 1
}
return {
count,
increment,
}
})
Pinia 没有 Mutation 层,Action 可以同步或异步修改状态。它也能在 Vue 2.7 或安装 Composition API 插件的 Vue 2 项目中使用,因此不能只凭 Pinia 判断 Vue 大版本。
28.2 Vue Router 常用变化
Vue Router 3 创建实例:
js
const router = new VueRouter({
mode: 'history',
routes,
})
Vue Router 4:
js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes,
})
组件中使用:
vue
<script setup>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
/**
* 跳转到用户详情页。
*
* @param {number} userId 用户 ID
* @returns {Promise<void>} 路由跳转完成后的 Promise
*/
async function openUser(userId) {
await router.push({
name: 'user-detail',
params: {
id: userId,
},
})
}
</script>
不要解构 route.params 后期待它自动响应后续路由变化,必要时使用计算属性:
js
const userId = computed(() => route.params.id)
29. 一个完整的增删改查对照示例
下面用"用户列表"展示两种风格如何组织同一功能。
29.1 Vue 2 Options API
vue
<script>
export default {
name: 'UserManage',
data() {
return {
keyword: '',
loading: false,
nextId: 3,
users: [
{ id: 1, name: '张三', enabled: true },
{ id: 2, name: '李四', enabled: false },
],
}
},
computed: {
filteredUsers() {
const keyword = this.keyword.trim()
if (!keyword) {
return this.users
}
return this.users.filter((user) => {
return user.name.includes(keyword)
})
},
},
methods: {
addUser() {
const name = this.keyword.trim()
if (!name) {
return
}
this.users.push({
id: this.nextId,
name,
enabled: true,
})
this.nextId += 1
this.keyword = ''
},
toggleUser(userId) {
const user = this.users.find(item => item.id === userId)
if (user) {
user.enabled = !user.enabled
}
},
deleteUser(userId) {
this.users = this.users.filter(user => user.id !== userId)
},
},
}
</script>
<template>
<main class="user_manage">
<form @submit.prevent="addUser">
<input v-model.trim="keyword" placeholder="输入姓名">
<button type="submit" :disabled="!keyword">
新增
</button>
</form>
<p v-if="loading">加载中...</p>
<ul v-else-if="filteredUsers.length">
<li v-for="user in filteredUsers" :key="user.id">
<span>{{ user.name }}</span>
<span>{{ user.enabled ? '启用' : '停用' }}</span>
<button type="button" @click="toggleUser(user.id)">
切换状态
</button>
<button type="button" @click="deleteUser(user.id)">
删除
</button>
</li>
</ul>
<p v-else>暂无用户</p>
</main>
</template>
29.2 Vue 3 Composition API
vue
<script setup lang="ts">
import { computed, ref } from 'vue'
interface User {
id: number
name: string
enabled: boolean
}
const keyword = ref('')
const loading = ref(false)
const nextId = ref(3)
const users = ref<User[]>([
{ id: 1, name: '张三', enabled: true },
{ id: 2, name: '李四', enabled: false },
])
const filteredUsers = computed(() => {
const normalizedKeyword = keyword.value.trim()
if (!normalizedKeyword) {
return users.value
}
return users.value.filter((user) => {
return user.name.includes(normalizedKeyword)
})
})
/**
* 使用当前输入的姓名新增用户,并清空输入框。
*
* @returns 无返回值;姓名为空时不执行新增
*/
function addUser() {
const name = keyword.value.trim()
if (!name) {
return
}
users.value.push({
id: nextId.value,
name,
enabled: true,
})
nextId.value += 1
keyword.value = ''
}
/**
* 切换指定用户的启用状态。
*
* @param userId 需要切换状态的用户 ID
* @returns 无返回值;用户不存在时不执行修改
*/
function toggleUser(userId: number) {
const user = users.value.find(item => item.id === userId)
if (user) {
user.enabled = !user.enabled
}
}
/**
* 从列表中删除指定用户。
*
* @param userId 需要删除的用户 ID
* @returns 无返回值
*/
function deleteUser(userId: number) {
users.value = users.value.filter(user => user.id !== userId)
}
</script>
<template>
<main class="user_manage">
<form @submit.prevent="addUser">
<input v-model.trim="keyword" placeholder="输入姓名">
<button type="submit" :disabled="!keyword">
新增
</button>
</form>
<p v-if="loading">加载中...</p>
<ul v-else-if="filteredUsers.length">
<li v-for="user in filteredUsers" :key="user.id">
<span>{{ user.name }}</span>
<span>{{ user.enabled ? '启用' : '停用' }}</span>
<button type="button" @click="toggleUser(user.id)">
切换状态
</button>
<button type="button" @click="deleteUser(user.id)">
删除
</button>
</li>
</ul>
<p v-else>暂无用户</p>
</main>
</template>
模板几乎没有变化,主要区别集中在脚本:
| Vue 2 | Vue 3 |
|---|---|
状态放在 data() |
状态由 ref() 创建 |
派生值放在 computed |
调用 computed() |
方法放在 methods |
直接声明函数 |
通过 this 访问成员 |
直接使用变量,通过 .value 访问 ref |
| JavaScript 为主 | TypeScript 类型更加自然 |
30. Vue 2 项目迁移 Vue 3 的推荐步骤
不要一上来就把所有 Options API 改成 Composition API。版本升级和代码风格改写是两件事,同时进行会扩大排错范围。
30.1 升级前盘点
- 确认 Node.js 和构建工具版本。
- 统计 Vue、Router、Vuex、UI 库和插件版本。
- 检查第三方库是否支持 Vue 3。
- 搜索
$on、$off、$once事件总线。 - 搜索
.native、.sync、Filter 和按键码。 - 搜索
Vue.set、this.$set、Vue.delete。 - 搜索全局
Vue.use、Vue.component、Vue.prototype。 - 搜索自定义指令和 Render 函数。
- 检查依赖
$children、$listeners的封装组件。 - 为登录、路由、表单和核心业务补充回归测试。
30.2 推荐迁移顺序
- 先升级工具链和 Vue 核心,保持 Options API。
- 迁移
new Vue()为createApp()。 - 迁移 Router、状态管理和 UI 组件库。
- 修复模板废弃语法与
v-model契约。 - 修复插件、全局 API、指令和 Render 函数。
- 完成业务回归测试。
- 最后按业务模块逐步引入
<script setup>和 Composables。
30.3 可以使用兼容构建版
大型 Vue 2 项目可以评估 Vue 3 的迁移构建版本 @vue/compat,它能为部分 Vue 2 行为提供兼容并输出警告。
但它不是长期解决方案:
- 第三方依赖仍需确认兼容性。
- 警告需要逐项消除。
- 已移除或行为改变的 API 仍需人工验证。
- 最终目标应是关闭兼容模式。
30.4 每批迁移都要验证
- 页面能否正常进入和离开。
- 表单输入与校验是否正常。
- 组件
v-model是否双向同步。 - 弹窗、下拉框等是否正确挂载。
- 路由参数和权限守卫是否正常。
- 列表的新增、删除、排序是否错位。
- 事件监听与定时器是否正确清理。
- 构建、单元测试和关键业务流程是否通过。
31. 新手学习与使用技巧
31.1 阅读 Vue 2 代码的方法
看到一个 Vue 2 组件时,按以下顺序:
- 看
props:组件从父组件接收什么。 - 看
data:组件自己保存什么状态。 - 看
computed:哪些值由状态计算得出。 - 看
watch:状态变化会触发什么副作用。 - 看
methods:用户操作和业务方法在哪里。 - 看生命周期:什么时候请求数据、绑定和清理资源。
- 回到模板:事件如何触发方法,状态如何渲染。
遇到 this.xxx 时,可以在 props、data、computed、methods、注入内容或全局实例属性中寻找来源。
31.2 阅读 Vue 3 代码的方法
看到 <script setup> 时,按业务功能阅读:
- 看
import:外部能力来自哪里。 - 看
defineProps和defineEmits:组件输入和输出。 - 看
ref、reactive:本地状态。 - 看
computed:派生状态。 - 看
watch:副作用。 - 看
onMounted等钩子:生命周期行为。 - 看普通函数:用户操作。
- 看模板如何消费这些变量和函数。
31.3 ref 还是 reactive
可以先记住这套实用规则:
| 场景 | 推荐 |
|---|---|
| 数字、字符串、布尔值 | ref() |
| 需要整体替换的数组或对象 | ref() |
| 结构固定的复杂表单 | reactive() |
| 暂时拿不准 | ref() |
不要在同一个项目里频繁无理由切换风格。统一性比争论某一种 API 绝对更好更重要。
31.4 computed 还是 watch
text
想得到一个新值? -> computed
想在变化后做一件事? -> watch
想自动追踪多项依赖? -> watchEffect
想响应一次点击? -> 普通事件方法
例如:
- 根据单价和数量算总价:
computed。 - 关键词变化后请求接口:
watch。 - 点击保存按钮提交表单:普通函数。
31.5 状态放在哪里
从最近、最小的范围开始:
text
只在当前组件使用 -> ref / reactive
父子组件共享 -> Props + Emit / v-model
当前组件树跨多层共享 -> provide / inject
跨页面共享业务状态 -> Pinia
需要分享和刷新后恢复 -> 路由参数或持久化存储
不要把所有变量都放进全局 Store。弹窗显示、输入框文字、hover 状态通常只属于当前组件。
31.6 常用调试技巧
- 使用 Vue Devtools 查看组件树、Props、事件和 Pinia 状态。
- 在编辑器中按
F12跳到定义,不要凭函数名猜来源。 - 模板没有更新时,先确认修改的是响应式值。
- Vue 3 脚本中检查是否漏写
.value。 - 组件事件无效时,检查事件名、参数和
defineEmits。 v-model无效时,对照 Prop 名与update:*事件名。- 列表显示错位时,检查
key是否稳定。 - 组件卸载后仍触发逻辑时,检查事件、定时器和请求是否清理。
31.7 不要滥用的写法
- 不要直接修改 Props。
- 不要同时在一个元素上写
v-if和v-for。 - 不要在计算属性里发请求或修改其他状态。
- 不要为了共享一个局部变量就创建全局 Store。
- 不要用模板引用代替正常的 Props / Emit 数据流。
- 不要把所有 Options API 代码机械转换成 Composition API。
- 不要为了追求"新语法"一次性重写稳定的老项目。
32. 常见报错与排查方式
32.1 Cannot read properties of undefined (reading 'xxx')
可能原因:
- 异步数据尚未返回。
- 对象初始值不完整。
- Vue 2 的
this指向错误。 - Props 没有传入。
处理方式:
vue
<p v-if="user">{{ user.name }}</p>
或在脚本中使用合理初始值和可选链:
js
const user = ref(null)
const userName = computed(() => user.value?.name ?? '')
32.2 Vue 3 脚本中状态不变化
错误:
js
const count = ref(0)
count += 1
正确:
js
count.value += 1
模板中则直接写:
vue
<button @click="count += 1">{{ count }}</button>
32.3 解构后不再响应
错误:
js
const state = reactive({ count: 0 })
const { count } = state
正确:
js
const { count } = toRefs(state)
或者直接使用 state.count。
32.4 v-model 没有同步
检查契约:
| 父组件 | 子组件 Prop | 子组件事件 |
|---|---|---|
Vue 2 v-model |
value |
input |
Vue 3 v-model |
modelValue |
update:modelValue |
Vue 3 v-model:show |
show |
update:show |
名称和大小写必须对应。
32.5 Vue 2 新增字段后页面不更新
js
// 可能不响应
this.user.age = 18
// Vue 2 正确方式
this.$set(this.user, 'age', 18)
也可以提前在 data() 中声明完整字段。
32.6 Vue 3 多根节点的 class 没有生效
多根节点组件无法自动判断属性落点。使用 v-bind="$attrs" 明确绑定到目标根元素,并根据需要设置 inheritAttrs: false。
33. 高频语法速查表
| 需求 | Vue 2 | Vue 3 |
|---|---|---|
| 声明状态 | data() { return { count: 0 } } |
const count = ref(0) |
| 脚本读取状态 | this.count |
count.value |
| 模板读取状态 | {``{ count }} |
{``{ count }} |
| 声明方法 | methods: { save() {} } |
function save() {} |
| 计算属性 | computed: { total() {} } |
const total = computed(() => {}) |
| 监听值 | watch: { count() {} } |
watch(count, () => {}) |
| 挂载完成 | mounted() |
onMounted(() => {}) |
| 卸载前 | beforeDestroy() |
onBeforeUnmount(() => {}) |
| 接收 Props | props: { title: String } |
defineProps<{ title: string }>() |
| 发送事件 | this.$emit('save', data) |
emit('save', data) |
| 默认双向 Prop | value |
modelValue |
| 默认双向事件 | input |
update:modelValue |
| 多个双向值 | .sync 或自定义 |
多个 v-model:xxx |
| 获取模板引用 | this.$refs.editor |
editorRef.value |
| 暴露子组件方法 | 默认实例可访问 | defineExpose() |
| 逻辑复用 | Mixins | Composables |
| 新增响应字段 | this.$set() |
直接赋值 |
| 删除响应字段 | this.$delete() |
delete |
| 全局插件 | Vue.use() |
app.use() |
| 全局组件 | Vue.component() |
app.component() |
| 全局实例属性 | Vue.prototype |
app.config.globalProperties |
| 格式化模板值 | Filter | 普通函数 / computed |
| 原生组件事件 | .native |
普通事件透传或显式 Emit |
| 更新 Prop 语法 | .sync |
v-model:propName |
34. 最后记住这十条
- Vue 2 和 Vue 3 的模板基础语法大部分相同,脚本组织方式变化更明显。
- Vue 3 支持 Options API,升级版本不等于必须重写所有组件。
- Vue 3 新项目优先使用 Composition API、
<script setup>和 TypeScript。 ref在脚本中使用.value,在模板中通常自动解包。reactive不要随意整体替换,解构时注意响应式丢失。- Props 只读,子组件通过 Emit 表达修改意图。
- Vue 3 默认
v-model是modelValue+update:modelValue。 - 派生值使用
computed,副作用使用watch,用户操作使用普通方法。 - 局部状态留在组件中,跨页面业务状态再考虑 Pinia。
- 迁移老项目时先保证行为兼容,再逐步改善代码组织方式。
建议的学习顺序:
text
HTML / CSS / JavaScript 基础
↓
Vue 模板语法
↓
ref、reactive、computed
↓
Props、Emit、v-model、Slot
↓
watch 与生命周期
↓
Router、Pinia、请求封装
↓
Composables 与 TypeScript
掌握 Vue 的关键不是背完所有 API,而是始终能回答三个问题:数据从哪里来、由谁修改、变化后影响哪些界面。只要数据流清楚,不论面对 Vue 2 还是 Vue 3,阅读和维护代码都会容易很多。