# Vue 2 迁移到 Vue 3 完整指南

# 目录

  1. 破坏性变更概览
  2. 全局API变化
  3. 模板指令变化
  4. 组件变化
  5. 生命周期钩子变化
  6. 移除的API
  7. 新增特性
  8. Composition API
  9. 其他重要变更
  10. 迁移工具

# 破坏性变更概览

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')
1
2
3
4
5
6

Vue 3:

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
1
2
3
4

# 2. 全局配置

Vue 2:

Vue.prototype.$http = axios
Vue.config.ignoredElements = ['my-custom-element']
1
2

Vue 3:

const app = createApp(App)
app.config.globalProperties.$http = axios
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('my-')
1
2
3

# 3. 全局API方法

Vue 2:

Vue.component('MyComponent', {})
Vue.directive('my-directive', {})
Vue.mixin({})
Vue.use(plugin)
1
2
3
4

Vue 3:

const app = createApp(App)
app.component('MyComponent', {})
app.directive('my-directive', {})
app.mixin({})
app.use(plugin)
1
2
3
4
5

# 4. nextTick 变化

Vue 2:

this.$nextTick(() => {})
Vue.nextTick(() => {})
1
2

Vue 3:

import { nextTick } from 'vue'

nextTick(() => {})
// 或在组件内
import { nextTick } from 'vue'
nextTick(() => {})
1
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>
1
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>
1
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>
1
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>
1
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> -->
1
2
3

Vue 3:

<!-- id 不会被覆盖 -->
<div id="a" v-bind="{ id: 'b' }"></div>
<!-- 结果: <div id="a"></div> -->
1
2
3

# 3. v-if 与 v-for 优先级

Vue 2:

<!-- v-for 优先级更高 -->
<div v-for="item in list" v-if="item.isActive"></div>
<!-- 会遍历整个列表后再判断 v-if -->
1
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>
1
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>
1
2
3
4
5
6
7
8
9

Vue 3:

<!-- 父组件 -->
<MyButton @click="handleClick" />

<!-- 子组件 -->
<script>
export default {
  emits: ['click'] // 声明后为组件事件
}
// 未在 emits 中声明的事件监听器会被添加到组件根元素上
</script>
1
2
3
4
5
6
7
8
9
10

# 5. 移除 .sync 修饰符

Vue 2:

<MyComponent :title.sync="title" />
<!-- 等价于 -->
<MyComponent :title="title" @update:title="title = $event" />
1
2
3

Vue 3:

<MyComponent v-model:title="title" />
<!-- 替换为 v-model:参数名 -->
1
2

# 组件变化

# 1. 函数式组件

Vue 2:

<template functional>
  <div>{{ props.title }}</div>
</template>

<script>
export default {
  props: ['title']
}
</script>
1
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>
1
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
})
1
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
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3. 自定义元素检测

Vue 2:

Vue.config.ignoredElements = ['my-custom-element', /^ion-/]
1

Vue 3:

const app = createApp(App)
app.config.compilerOptions.isCustomElement = tag => {
  return tag.startsWith('my-') || tag.startsWith('ion-')
}
1
2
3
4

# 4. 插槽变化

Vue 2:

<!-- 具名插槽 -->
<template slot="header">
  <h1>标题</h1>
</template>

<!-- 作用域插槽 -->
<template slot-scope="{ user }">
  <div>{{ user.name }}</div>
</template>
1
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>
1
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>
1
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('组件已卸载')
  }
}
1
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('组件已卸载')
    })
  }
}
1
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>
1
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>
1
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)
1
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)
1
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>
1
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>
1
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>
1
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>
1
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>
1
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>
1
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>
1
2
3
4
5
6

Vue 3:

<!-- inline-template 已移除 -->
<!-- 改用其他方式,如动态组件或插槽 -->
<my-component>
  <template #default>
    <div>
      <p>这些内容作为插槽内容</p>
    </div>
  </template>
</my-component>
1
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>
1
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>
1
2
3
4
5
6

Vue 3 (多根节点):

<template>
  <h1>标题</h1>
  <p>内容</p>
</template>
1
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>
1
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
    }
  }
})
1
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)
  }
}
1
2
3
4
5

Vue 3:

import { h } from 'vue'

export default {
  render() {
    return h('div', this.message)
  }
}
1
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>
1
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}`)
})
1
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)
    })
  }
}
1
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
    }
  }
}
1
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>
1
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
    }
  }
}
1
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) {
    // 指令与元素解绑时
  }
})
1
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 时触发
})
1
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;
}
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;
}
1
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>
1
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')
    ])
  }
}
1
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')
    ])
  }
}
1
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>
1
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>
1
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>
1
2
3
4
5
6
7
8
9
10
11
12

# 6. DevTools

Vue 2:

Vue.config.devtools = true
1

Vue 3:

const app = createApp(App)
app.config.devtools = true

// 仅在开发环境启用
if (process.env.NODE_ENV !== 'production') {
  app.config.devtools = true
}
1
2
3
4
5
6
7

# 7. 错误处理

Vue 2:

Vue.config.errorHandler = (err, vm, info) => {
  console.error('Vue error:', err, info)
}
1
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)
}
1
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' // 只导入需要的
1
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
            }
          }
        }
      })
  }
}
1
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
1
import { createApp } from '@vue/compat'

const app = createApp(App)

// 配置兼容模式
app.config.compilerOptions.compatConfig = {
  MODE: 2, // Vue 2 兼容模式
  GLOBAL_MOUNT: false, // 禁用特定特性的兼容
}
1
2
3
4
5
6
7
8
9

# 3. 迁移检查清单

  • [ ] 全局 API 重构
  • [ ] v-model 用法更新
  • [ ] 移除过滤器
  • [ ] 生命周期钩子重命名
  • [ ] 事件 API 替换
  • [ ] 插槽语法更新
  • [ ] 自定义指令更新
  • [ ] 过渡类名更新
  • [ ] 移除 .sync 和 .native
  • [ ] $listeners 合并到 $attrs
  • [ ] emits 选项声明
  • [ ] 异步组件更新
  • [ ] 渲染函数更新
  • [ ] 安装 Vue DevTools beta

# 4. 迁移步骤建议

  1. 准备工作

    • 升级 Node.js 到最新 LTS 版本
    • 更新 package.json 中的依赖
    • 安装 Vue 3 和相关生态库
  2. 使用迁移构建

    • 安装 @vue/compat
    • 配置兼容模式
    • 修复编译警告
  3. 逐步迁移

    • 按照警告信息修复代码
    • 每修复一个模块就测试
    • 逐步启用 Vue 3 新特性
  4. 最终清理

    • 移除 @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>
1
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)
1
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>
1
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>
1
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
    }
  }
}
1
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 - 更小的最终代码

迁移建议:

  1. 使用迁移构建逐步过渡
  2. 优先修复编译警告
  3. 逐步采用 Composition API
  4. 充分测试每个变更
  5. 利用新特性优化代码

参考资源: