# Vue 2 迁移到 Vue 3 完整指南
# 目录
# 破坏性变更概览
Vue 3 相对于 Vue 2 有以下主要破坏性变更:
- 全局 API 重构
- 模板指令变化
- 组件模型变化
- 渲染函数变化
- 自定义指令 API 变化
- 移除部分 API 和特性
# 全局API变化
# 1. 创建应用实例
Vue 2:
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App)
}).$mount('#app')
2
3
4
5
6
Vue 3:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
2
3
4
# 2. 全局配置
Vue 2:
Vue.prototype.$http = axios
Vue.config.ignoredElements = ['my-custom-element']
2
Vue 3:
const app = createApp(App)
app.config.globalProperties.$http = axios
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('my-')
2
3
# 3. 全局API方法
Vue 2:
Vue.component('MyComponent', {})
Vue.directive('my-directive', {})
Vue.mixin({})
Vue.use(plugin)
2
3
4
Vue 3:
const app = createApp(App)
app.component('MyComponent', {})
app.directive('my-directive', {})
app.mixin({})
app.use(plugin)
2
3
4
5
# 4. nextTick 变化
Vue 2:
this.$nextTick(() => {})
Vue.nextTick(() => {})
2
Vue 3:
import { nextTick } from 'vue'
nextTick(() => {})
// 或在组件内
import { nextTick } from 'vue'
nextTick(() => {})
2
3
4
5
6
# 模板指令变化
# 1. v-model 变化
Vue 2 (组件上):
<!-- 父组件 -->
<CustomInput v-model="value" />
<!-- 子组件 -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props: ['value']
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
Vue 3 (组件上):
<!-- 父组件 -->
<CustomInput v-model="value" />
<!-- 子组件 -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue 3 自定义 v-model 参数名:
<!-- 父组件 -->
<CustomInput v-model:title="title" />
<!-- 子组件 -->
<template>
<input :value="title" @input="$emit('update:title', $event.target.value)" />
</template>
<script>
export default {
props: ['title'],
emits: ['update:title']
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue 3 多个 v-model 绑定:
<!-- 父组件 -->
<UserForm v-model:firstName="firstName" v-model:lastName="lastName" />
<!-- 子组件 -->
<script>
export default {
props: ['firstName', 'lastName'],
emits: ['update:firstName', 'update:lastName']
}
</script>
2
3
4
5
6
7
8
9
10
# 2. v-bind 合并行为
Vue 2:
<!-- id 会被覆盖 -->
<div id="a" v-bind="{ id: 'b' }"></div>
<!-- 结果: <div id="b"></div> -->
2
3
Vue 3:
<!-- id 不会被覆盖 -->
<div id="a" v-bind="{ id: 'b' }"></div>
<!-- 结果: <div id="a"></div> -->
2
3
# 3. v-if 与 v-for 优先级
Vue 2:
<!-- v-for 优先级更高 -->
<div v-for="item in list" v-if="item.isActive"></div>
<!-- 会遍历整个列表后再判断 v-if -->
2
3
Vue 3:
<!-- v-if 优先级更高 -->
<div v-for="item in list" v-if="item.isActive"></div>
<!-- 报错: v-if 不能访问 v-for 的 item 变量 -->
<!-- 正确做法 -->
<template v-for="item in list" :key="item.id">
<div v-if="item.isActive"></div>
</template>
<!-- 或使用计算属性 -->
<div v-for="item in activeList" :key="item.id"></div>
2
3
4
5
6
7
8
9
10
11
# 4. 移除 v-on.native 修饰符
Vue 2:
<!-- 父组件 -->
<MyButton @click.native="handleClick" />
<!-- 子组件 -->
<script>
export default {
emits: ['click'] // 不声明则会作为原生事件
}
</script>
2
3
4
5
6
7
8
9
Vue 3:
<!-- 父组件 -->
<MyButton @click="handleClick" />
<!-- 子组件 -->
<script>
export default {
emits: ['click'] // 声明后为组件事件
}
// 未在 emits 中声明的事件监听器会被添加到组件根元素上
</script>
2
3
4
5
6
7
8
9
10
# 5. 移除 .sync 修饰符
Vue 2:
<MyComponent :title.sync="title" />
<!-- 等价于 -->
<MyComponent :title="title" @update:title="title = $event" />
2
3
Vue 3:
<MyComponent v-model:title="title" />
<!-- 替换为 v-model:参数名 -->
2
# 组件变化
# 1. 函数式组件
Vue 2:
<template functional>
<div>{{ props.title }}</div>
</template>
<script>
export default {
props: ['title']
}
</script>
2
3
4
5
6
7
8
9
Vue 3:
<template>
<div>{{ title }}</div>
</template>
<script>
export default {
props: ['title'],
functional: true // 仅在使用 options API 时需要
}
</script>
<!-- 或使用函数式写法 -->
<script>
import { h } from 'vue'
export default function FunctionalComponent(props, { slots }) {
return h('div', props.title)
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2. 异步组件
Vue 2:
const AsyncComponent = () => import('./AsyncComponent.vue')
// 或带配置
const AsyncComponent = () => ({
component: import('./AsyncComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 200,
timeout: 3000
})
2
3
4
5
6
7
8
9
10
Vue 3:
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
)
// 带配置
const AsyncComponent = defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3. 自定义元素检测
Vue 2:
Vue.config.ignoredElements = ['my-custom-element', /^ion-/]
Vue 3:
const app = createApp(App)
app.config.compilerOptions.isCustomElement = tag => {
return tag.startsWith('my-') || tag.startsWith('ion-')
}
2
3
4
# 4. 插槽变化
Vue 2:
<!-- 具名插槽 -->
<template slot="header">
<h1>标题</h1>
</template>
<!-- 作用域插槽 -->
<template slot-scope="{ user }">
<div>{{ user.name }}</div>
</template>
2
3
4
5
6
7
8
9
Vue 3:
<!-- 具名插槽 -->
<template v-slot:header>
<h1>标题</h1>
</template>
<!-- 或简写 -->
<template #header>
<h1>标题</h1>
</template>
<!-- 作用域插槽 -->
<template v-slot:default="{ user }">
<div>{{ user.name }}</div>
</template>
<!-- 或简写 -->
<template #default="{ user }">
<div>{{ user.name }}</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5. emits 选项
Vue 3 新增:
<script>
export default {
emits: {
// 无验证
click: null,
// 带验证
submit: payload => {
if (payload.email && payload.password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm() {
this.$emit('submit', { email: 'test@test.com', password: '123456' })
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 生命周期钩子变化
# 钩子名称变化
| Vue 2 | Vue 3 |
|---|---|
| beforeCreate | beforeCreate (使用 setup 替代) |
| created | created (使用 setup 替代) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeDestroy | beforeUnmount |
| destroyed | unmounted |
| errorCaptured | onErrorCaptured |
# Options API 中
Vue 3:
export default {
beforeUnmount() {
console.log('组件即将卸载')
},
unmounted() {
console.log('组件已卸载')
}
}
2
3
4
5
6
7
8
# Composition API 中
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('组件即将挂载')
})
onMounted(() => {
console.log('组件已挂载')
})
onBeforeUnmount(() => {
console.log('组件即将卸载')
})
onUnmounted(() => {
console.log('组件已卸载')
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 移除的API
# 1. 过滤器 (Filters)
Vue 2:
<template>
<div>{{ message | capitalize }}</div>
<div :title="message | capitalize"></div>
</template>
<script>
export default {
filters: {
capitalize(value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue 3 替代方案:
<!-- 使用计算属性 -->
<template>
<div>{{ capitalizedMessage }}</div>
</template>
<script>
export default {
props: ['message'],
computed: {
capitalizedMessage() {
if (!this.message) return ''
return this.message.charAt(0).toUpperCase() + this.message.slice(1)
}
}
}
</script>
<!-- 或使用方法 -->
<template>
<div>{{ capitalize(message) }}</div>
</template>
<script>
export default {
methods: {
capitalize(value) {
if (!value) return ''
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
}
</script>
<!-- 全局过滤器改为全局方法 -->
// main.js
const app = createApp(App)
app.config.globalProperties.$filters = {
capitalize(value) {
if (!value) return ''
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
// 组件中使用
<template>
<div>{{ $filters.capitalize(message) }}</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 2. 事件API ($on, $off, $once)
Vue 2:
// 事件总线
const eventBus = new Vue()
eventBus.$on('my-event', handler)
eventBus.$emit('my-event', payload)
eventBus.$off('my-event', handler)
eventBus.$once('my-event', handler)
// 组件内
this.$on('my-event', handler)
this.$off('my-event', handler)
this.$once('my-event', handler)
2
3
4
5
6
7
8
9
10
11
Vue 3 替代方案:
// 使用第三方库 mitt
import mitt from 'mitt'
const emitter = mitt()
emitter.on('my-event', handler)
emitter.emit('my-event', payload)
emitter.off('my-event', handler)
// 或使用提供的 eventEmitter
import { createApp } from 'vue'
const app = createApp(App)
app.config.globalProperties.$emitter = mitt()
// 组件内使用
this.$emitter.on('my-event', handler)
this.$emitter.emit('my-event', payload)
this.$emitter.off('my-event', handler)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3. $children
Vue 2:
<script>
export default {
mounted() {
console.log(this.$children) // 访问子组件实例
}
}
</script>
2
3
4
5
6
7
Vue 3 替代方案:
<template>
<ChildComponent ref="childRef" />
</template>
<script>
export default {
mounted() {
console.log(this.$refs.childRef) // 使用 ref 访问子组件
}
}
</script>
<!-- 或使用 provide/inject -->
<!-- 父组件 -->
<script>
export default {
provide() {
return {
parentInstance: this
}
}
}
</script>
<!-- 子组件 -->
<script>
export default {
inject: ['parentInstance']
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 4. $listeners
Vue 2:
<!-- 将所有事件监听器传递给子组件 -->
<input v-on="$listeners" />
<script>
export default {
computed: {
inputListeners() {
return {
...this.$listeners,
input: event => this.$emit('input', event.target.value)
}
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue 3:
<!-- $listeners 已移除,合并到 $attrs -->
<input v-bind="$attrs" />
<script>
export default {
inheritAttrs: false
}
</script>
2
3
4
5
6
7
8
# 5. $scopedSlots
Vue 2:
<template>
<div>
<slot name="header" :user="user"></slot>
</div>
</template>
<script>
export default {
computed: {
headerSlot() {
return this.$scopedSlots.header
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue 3:
<template>
<div>
<slot name="header" :user="user"></slot>
</div>
</template>
<script>
export default {
computed: {
headerSlot() {
return this.$slots.header // 统一为 $slots
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6. 内联模板 (inline-template)
Vue 2:
<my-component inline-template>
<div>
<p>这些内容将作为组件的模板</p>
<p>而不是作为分发内容</p>
</div>
</my-component>
2
3
4
5
6
Vue 3:
<!-- inline-template 已移除 -->
<!-- 改用其他方式,如动态组件或插槽 -->
<my-component>
<template #default>
<div>
<p>这些内容作为插槽内容</p>
</div>
</template>
</my-component>
2
3
4
5
6
7
8
9
# 新增特性
# 1. Teleport
<template>
<button @click="showModal = true">打开模态框</button>
<Teleport to="body">
<div v-if="showModal" class="modal">
<p>这是一个模态框</p>
<button @click="showModal = false">关闭</button>
</div>
</Teleport>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const showModal = ref(false)
return { showModal }
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2. Fragments (多根节点)
Vue 2 (单根节点):
<template>
<div>
<h1>标题</h1>
<p>内容</p>
</div>
</template>
2
3
4
5
6
Vue 3 (多根节点):
<template>
<h1>标题</h1>
<p>内容</p>
</template>
2
3
4
# 3. Suspense
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./AsyncComponent.vue')
)
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 4. defineComponent
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
name: 'MyComponent',
props: {
title: String
},
setup(props) {
const count = ref(0)
const doubled = computed(() => count.value * 2)
return {
count,
doubled
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5. h 函数
Vue 2:
export default {
render(h) {
return h('div', this.message)
}
}
2
3
4
5
Vue 3:
import { h } from 'vue'
export default {
render() {
return h('div', this.message)
}
}
2
3
4
5
6
7
# Composition API
# 1. setup 函数
<template>
<div>{{ count }} - {{ doubled }}</div>
<button @click="increment">增加</button>
</template>
<script>
import { ref, computed } from 'vue'
export default {
props: {
initialCount: {
type: Number,
default: 0
}
},
setup(props) {
const count = ref(props.initialCount)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
doubled,
increment
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 2. 响应式 API
import { ref, reactive, computed, watch, watchEffect } from 'vue'
// ref - 用于基本类型
const count = ref(0)
console.log(count.value) // 访问值需要 .value
// reactive - 用于对象
const state = reactive({
name: 'Vue 3',
version: '3.x'
})
console.log(state.name) // 直接访问,不需要 .value
// computed
const doubled = computed(() => count.value * 2)
// watch - 监听特定数据源
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})
// watch 监听多个数据源
watch([count, doubled], ([newCount, newDoubled], [oldCount, oldDoubled]) => {
console.log(`count: ${oldCount} -> ${newCount}`)
})
// watch 监听 reactive 对象的属性
watch(
() => state.name,
(newName, oldName) => {
console.log(`name: ${oldName} -> ${newName}`)
}
)
// watchEffect - 自动追踪依赖
watchEffect(() => {
console.log(`count 的值是: ${count.value}`)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 3. 生命周期钩子
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onRenderTracked,
onRenderTriggered
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('组件即将挂载')
})
onMounted(() => {
console.log('组件已挂载')
})
onBeforeUnmount(() => {
console.log('组件即将卸载')
})
onUnmounted(() => {
console.log('组件已卸载')
})
// 新增的调试钩子
onRenderTracked((e) => {
console.log('组件渲染追踪', e)
})
onRenderTriggered((e) => {
console.log('组件渲染触发', e)
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 4. 依赖注入 (provide/inject)
// 父组件
import { provide, ref } from 'vue'
export default {
setup() {
const theme = ref('dark')
const updateTheme = (newTheme) => {
theme.value = newTheme
}
provide('theme', theme)
provide('updateTheme', updateTheme)
}
}
// 子组件
import { inject } from 'vue'
export default {
setup() {
const theme = inject('theme', 'light') // 第二个参数是默认值
const updateTheme = inject('updateTheme')
return {
theme,
updateTheme
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 5. 模板引用 (ref)
<template>
<input ref="inputRef" />
<ChildComponent ref="childRef" />
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const inputRef = ref(null)
const childRef = ref(null)
onMounted(() => {
inputRef.value.focus()
console.log(childRef.value) // 子组件实例
})
return {
inputRef,
childRef
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 6. toRefs 和 toRef
import { reactive, toRefs, toRef } from 'vue'
export default {
setup() {
const state = reactive({
name: 'Vue',
version: '3'
})
// toRefs - 将响应式对象转换为普通对象,每个属性都是 ref
const { name, version } = toRefs(state)
// toRef - 为某个属性创建 ref
const nameRef = toRef(state, 'name')
return {
name,
version,
nameRef
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 其他重要变更
# 1. 自定义指令
Vue 2:
Vue.directive('my-directive', {
bind(el, binding, vnode) {
// 指令第一次绑定到元素时
},
inserted(el, binding, vnode) {
// 被绑定元素插入父节点时
},
update(el, binding, vnode, oldVnode) {
// VNode 更新时
},
componentUpdated(el, binding, vnode, oldVnode) {
// VNode 及其子 VNode 全部更新后
},
unbind(el, binding, vnode) {
// 指令与元素解绑时
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue 3:
const app = createApp(App)
app.directive('my-directive', {
created(el, binding, vnode, prevVnode) {
// 新增:在元素属性或事件监听器应用之前调用
},
beforeMount(el, binding, vnode, prevVnode) {
// 替代 bind
},
mounted(el, binding, vnode, prevVnode) {
// 替代 inserted
},
beforeUpdate(el, binding, vnode, prevVnode) {
// 新增:在元素父组件更新之前调用
},
updated(el, binding, vnode, prevVnode) {
// 替代 update 和 componentUpdated
},
beforeUnmount(el, binding, vnode, prevVnode) {
// 新增:在元素父组件卸载之前调用
},
unmounted(el, binding, vnode, prevVnode) {
// 替代 unbind
}
})
// 函数简写
app.directive('my-directive', (el, binding) => {
// 在 mounted 和 updated 时触发
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 2. 过渡类名变化
Vue 2:
.v-enter,
.v-leave-to {
opacity: 0;
}
.v-leave,
.v-enter-to {
opacity: 1;
}
2
3
4
5
6
7
8
9
Vue 3:
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-leave-from,
.v-enter-to {
opacity: 1;
}
/* 简写形式 */
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 3. 过渡组件变化
Vue 3:
<template>
<Transition>
<div v-if="show">内容</div>
</Transition>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</TransitionGroup>
</template>
<script>
// Transition 和 TransitionGroup 现在是内置组件
// 无需注册即可使用
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4. 渲染函数
Vue 2:
export default {
render(h) {
return h('div', {
class: ['my-class'],
style: { color: 'red' },
attrs: { id: 'my-id' },
props: { title: 'My Title' },
domProps: { innerHTML: 'content' },
on: { click: this.handleClick },
nativeOn: { click: this.handleNativeClick },
directives: [
{ name: 'my-directive', value: '123' }
],
slot: 'default',
key: 'my-key',
ref: 'myRef'
}, [
h('span', 'child')
])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Vue 3:
import { h } from 'vue'
export default {
render() {
return h('div', {
class: 'my-class',
style: { color: 'red' },
id: 'my-id',
title: 'My Title',
innerHTML: 'content',
onClick: this.handleClick,
'onUpdate:modelValue': (value) => this.$emit('update:modelValue', value),
key: 'my-key',
ref: 'myRef'
}, [
h('span', 'child')
])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 5. 单文件组件 (SFC) 变化
Vue 3:
<template>
<div>{{ message }}</div>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('Hello Vue 3')
// 无需 return,顶层变量自动暴露给模板
</script>
<style scoped>
div {
color: red;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defineProps 和 defineEmits:
<script setup>
// 定义 props
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
}
})
// 定义 emits
const emit = defineEmits(['update:title', 'submit'])
// 使用
emit('update:title', '新标题')
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defineExpose:
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
// 暴露给父组件
defineExpose({
count,
increment
})
</script>
2
3
4
5
6
7
8
9
10
11
12
# 6. DevTools
Vue 2:
Vue.config.devtools = true
Vue 3:
const app = createApp(App)
app.config.devtools = true
// 仅在开发环境启用
if (process.env.NODE_ENV !== 'production') {
app.config.devtools = true
}
2
3
4
5
6
7
# 7. 错误处理
Vue 2:
Vue.config.errorHandler = (err, vm, info) => {
console.error('Vue error:', err, info)
}
2
3
Vue 3:
const app = createApp(App)
app.config.errorHandler = (err, instance, info) => {
console.error('Vue error:', err, info)
}
// 新增: 警告处理器
app.config.warnHandler = (msg, instance, trace) => {
console.warn('Vue warning:', msg, trace)
}
2
3
4
5
6
7
8
9
10
# 8. 性能优化
Vue 3:
// 静态提升
// 编译器会自动提升静态节点
const hoisted = createStaticVNode('<div>静态内容</div>')
// 预字符串化
// 大量静态内容会被预字符串化
const staticContent = '<div>...</div>'
// 树摇优化
// 未使用的代码会被自动移除
import { ref, computed } from 'vue' // 只导入需要的
2
3
4
5
6
7
8
9
10
11
# 迁移工具
# 1. Vue Migration Build
Vue 3 提供了一个迁移构建版本,允许 Vue 2 的代码在 Vue 3 中运行:
// vue.config.js
module.exports = {
chainWebpack: config => {
config.resolve.alias.set('vue', '@vue/compat')
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2. @vue/compat
npm install @vue/compat
import { createApp } from '@vue/compat'
const app = createApp(App)
// 配置兼容模式
app.config.compilerOptions.compatConfig = {
MODE: 2, // Vue 2 兼容模式
GLOBAL_MOUNT: false, // 禁用特定特性的兼容
}
2
3
4
5
6
7
8
9
# 3. 迁移检查清单
- [ ] 全局 API 重构
- [ ] v-model 用法更新
- [ ] 移除过滤器
- [ ] 生命周期钩子重命名
- [ ] 事件 API 替换
- [ ] 插槽语法更新
- [ ] 自定义指令更新
- [ ] 过渡类名更新
- [ ] 移除 .sync 和 .native
- [ ] $listeners 合并到 $attrs
- [ ] emits 选项声明
- [ ] 异步组件更新
- [ ] 渲染函数更新
- [ ] 安装 Vue DevTools beta
# 4. 迁移步骤建议
准备工作
- 升级 Node.js 到最新 LTS 版本
- 更新 package.json 中的依赖
- 安装 Vue 3 和相关生态库
使用迁移构建
- 安装 @vue/compat
- 配置兼容模式
- 修复编译警告
逐步迁移
- 按照警告信息修复代码
- 每修复一个模块就测试
- 逐步启用 Vue 3 新特性
最终清理
- 移除 @vue/compat
- 删除 Vue 2 兼容代码
- 优化和重构
# 常见迁移问题
# 1. 如何处理过滤器?
使用计算属性或方法替代:
<!-- Vue 2 -->
<div>{{ price | currency }}</div>
<!-- Vue 3 -->
<div>{{ formatPrice(price) }}</div>
<script>
export default {
methods: {
formatPrice(value) {
return '$' + value.toFixed(2)
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2. 如何处理事件总线?
使用 mitt 或其他事件库:
// utils/eventBus.js
import mitt from 'mitt'
export default mitt()
// 使用
import emitter from './utils/eventBus'
emitter.emit('event', data)
emitter.on('event', handler)
2
3
4
5
6
7
8
# 3. 如何处理 $children?
使用 ref 和模板引用:
<template>
<ChildComponent ref="child" />
</template>
<script>
export default {
mounted() {
console.log(this.$refs.child)
}
}
</script>
2
3
4
5
6
7
8
9
10
11
# 4. 如何处理 v-model?
更新组件使用 modelValue 和 update:modelValue:
<!-- 父组件 -->
<CustomInput v-model="text" />
<!-- 子组件 -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 5. 如何迁移 Vuex?
Vuex 4 与 Vue 3 兼容,但推荐迁移到 Pinia:
// store/index.js (Pinia)
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
// store/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
email: ''
}),
getters: {
fullName: (state) => `${state.name} (${state.email})`
},
actions: {
updateUser(payload) {
this.name = payload.name
this.email = payload.email
}
}
})
// 组件中使用
import { useUserStore } from '@/store/user'
export default {
setup() {
const userStore = useUserStore()
return {
user: userStore.user,
updateUser: userStore.updateUser
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 总结
Vue 3 相比 Vue 2 带来了:
✅ 性能提升 - 更快的渲染、更小的打包体积 ✅ Composition API - 更好的逻辑复用和代码组织 ✅ TypeScript 支持 - 更好的类型推断 ✅ 新特性 - Teleport、Fragments、Suspense ✅ 更好的 Tree-shaking - 更小的最终代码
迁移建议:
- 使用迁移构建逐步过渡
- 优先修复编译警告
- 逐步采用 Composition API
- 充分测试每个变更
- 利用新特性优化代码
参考资源: