Vue3新特性与Composition API

目录


Vue3概述

Vue3主要新特性

特性 说明
Proxy响应式 使用Proxy替代Object.defineProperty
Composition API 新增组合式API,更灵活的代码组织
Teleport 原生支持将组件渲染到任意DOM
Suspense 原生支持异步组件loading状态
Fragment 组件可以有多根节点
更好的TypeScript支持 完整TypeScript支持
更小更快的体积 优化后的打包体积
更强大的Tree-shaking 更多API可以按需引入

Vue3 vs Vue2对比

方面 Vue2 Vue3
架构 Options API Composition API + Options API
响应式 Object.defineProperty Proxy
入口函数 new Vue() createApp()
根节点 只能有一个根节点 支持多根节点(Fragment)
TypeScript partial support 完整支持
打包体积 ~20KB ~10KB
虚拟DOM 完整重写 优化重写

创建Vue3项目

使用Vite创建

1
2
3
4
5
6
7
8
# npm
npm create vite@latest my-vue3-app -- --template vue

# yarn
yarn create vite my-vue3-app -- --template vue

# pnpm
pnpm create vite my-vue3-app -- --template vue

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
my-vue3-app/
├── public/
│ └── favicon.ico
├── src/
│ ├── assets/
│ │ └── logo.png
│ ├── components/
│ │ └── HelloWorld.vue
│ ├── App.vue
│ └── main.js
├── index.html
├── package.json
└── vite.config.js

main.js对比

1
2
3
4
5
6
7
// Vue2 - main.js
import Vue from 'vue'
import App from './App.vue'

new Vue({
render: h => h(App)
}).$mount('#app')
1
2
3
4
5
6
7
// Vue3 - main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.mount('#app')

Composition API

为什么需要Composition API

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
┌─────────────────────────────────────────────────────────────┐
│ Options API 问题 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ export default { │ │
│ │ data() { return { ... } }, │ │
│ │ methods: { ... }, │ │
│ │ computed: { ... }, │ │
│ │ watch: { ... } │ │
│ │ } │ │
│ │ │ │
│ │ 同一功能的代码被分散在各处 │ │
│ │ - 相关的mousePosition和setMousePosition不在一处 │ │
│ │ - 功能扩展时需要mixin,容易命名冲突 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Composition API 优势 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ export default { │ │
│ │ setup() { │ │
│ │ // 相关逻辑组织在一起 │ │
│ │ const { x, y } = useMousePosition() │ │
│ │ const { list } = useFetchData() │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ - 逻辑复用更清晰 │ │
│ │ - 更好的类型推断 │ │
│ │ - 更好的Tree-shaking │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

setup函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- Vue3 -->
<template>
<div>{{ message }}</div>
</template>

<script>
import { ref } from 'vue'

export default {
setup() {
// 响应式数据
const message = ref('Hello Vue3')

// setup返回的对象可以在模板中使用
return {
message
}
}
}
</script>

setup执行时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
setup() {
console.log('setup执行')
console.log(this) // undefined - setup中的this是undefined
}

// 生命周期钩子在setup中调用
import { onMounted, onUpdated, onUnmounted } from 'vue'

setup() {
onMounted(() => {
console.log('组件挂载完成')
})

onUpdated(() => {
console.log('组件更新')
})

onUnmounted(() => {
console.log('组件卸载')
})
}

setup与Options API共存

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
export default {
// Options API
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},

// Composition API
setup() {
const message = ref('Hello')

// setup中可以通过this访问Options API的数据
const increment = () => {
this.count++
}

return {
message,
increment
}
}
}

响应式API

ref和reactive

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
import { ref, reactive, readonly, shallowRef, shallowReactive } from 'vue'

// ref - 基本类型响应式
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1

// reactive - 对象响应式
const state = reactive({
count: 0,
user: { name: '张三' }
})
state.count++
state.user.name = '李四'

// readonly - 只读响应式
const original = reactive({ count: 0 })
const copy = readonly(original)

// shallowRef - 浅层响应式
const shallow = shallowRef({ count: 0 })
shallow.value.count = 1 // 不触发更新
shallow.value = { count: 1 } // 触发更新

// shallowReactive - 浅层reactive
const shallowState = shallowReactive({
count: 0,
deep: { name: '张三' }
})
shallowState.count = 1 // 触发更新
shallowState.deep.name = '李四' // 不触发更新

toRef和toRefs

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
import { toRef, toRefs, reactive } from 'vue'

const state = reactive({
count: 0,
message: 'Hello',
user: { name: '张三' }
})

// toRef - 将响应式对象的属性转换为ref
const countRef = toRef(state, 'count')
countRef.value++
console.log(state.count) // 1

// toRefs - 将响应式对象的所有属性转换为ref
const { count, message } = toRefs(state)
count.value++
message.value = 'Hi'

// 用于解构时保持响应式
function useFeature() {
const state = reactive({ ... })

// 解构后仍然响应式
return toRefs(state)
}

computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ref, computed } from 'vue'

const count = ref(0)

// 计算属性 - 只读
const doubled = computed(() => count.value * 2)

// 计算属性 - 可写
const plusOne = computed({
get: () => count.value + 1,
set: (val) => { count.value = val - 1 }
})

console.log(doubled.value) // 0
count.value = 2
console.log(doubled.value) // 4

watch和watchEffect

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
import { ref, reactive, watch, watchEffect } from 'vue'

const count = ref(0)
const user = reactive({ name: '张三', age: 18 })

// watch - 明确指定要监听的数据
watch(count, (newVal, oldVal) => {
console.log(`count变化: ${oldVal} -> ${newVal}`)
})

// watch - 监听多个数据
watch([count, () => user.name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`数据变化`)
})

// watch - 深度监听
watch(user, (newVal) => {
console.log('用户变化:', newVal)
}, { deep: true })

// watch - 立即执行
watch(count, (newVal) => {
console.log('立即执行:', newVal)
}, { immediate: true })

// watchEffect - 自动收集依赖
watchEffect(() => {
// 自动追踪count和user.name的变化
console.log(`count: ${count.value}, name: ${user.name}`)
})

// watchEffect - 清除副作用
watchEffect((onInvalidate) => {
const token = performAsyncOperation(count.value)

onInvalidate(() => {
token.cancel() // 清理操作
})
})

ref获取DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div ref="domRef">内容</div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
setup() {
const domRef = ref(null)

onMounted(() => {
console.log(domRef.value) // DOM元素
domRef.value.style.color = 'red'
})

return { domRef }
}
}
</script>

响应式检测

1
2
3
4
5
6
7
8
9
10
11
import { isRef, isReactive, isReadonly, isProxy } from 'vue'

const count = ref(0)
const state = reactive({ name: '张三' })
const copy = readonly(state)

console.log(isRef(count)) // true
console.log(isReactive(state)) // true
console.log(isReadonly(copy)) // true
console.log(isProxy(state)) // true
console.log(isProxy(copy)) // true

生命周期

生命周期对比

Vue2 Vue3 (onXxx) 说明
beforeCreate - setup()替代
created - setup()替代
beforeMount onBeforeMount 挂载前
mounted onMounted 挂载后
beforeUpdate onBeforeUpdate 更新前
updated onUpdated 更新后
beforeDestroy onBeforeUnmount 卸载前
destroyed onUnmounted 卸载后
errorCaptured onErrorCaptured 错误捕获
- onRenderTracked 渲染依赖追踪
- onRenderTriggered 渲染触发更新

生命周期使用示例

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
import { onMounted, onBeforeMount, onUpdated, onBeforeUpdate, onUnmounted, onBeforeUnmount } from 'vue'

export default {
setup() {
// setup等同于beforeCreate和created
console.log('setup执行')

onBeforeMount(() => {
console.log('onBeforeMount - 挂载前')
})

onMounted(() => {
console.log('onMounted - 挂载后')
// DOM已挂载,可以操作DOM
})

onBeforeUpdate(() => {
console.log('onBeforeUpdate - 更新前')
// 可以访问更新前的DOM
})

onUpdated(() => {
console.log('onUpdated - 更新后')
// DOM已更新
})

onBeforeUnmount(() => {
console.log('onBeforeUnmount - 卸载前')
// 清理定时器、事件监听等
})

onUnmounted(() => {
console.log('onUnmounted - 卸载后')
})

return {}
}
}

父子组件生命周期顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
挂载阶段:
Parent beforeCreate
Parent created
Parent beforeMount
Child beforeCreate
Child created
Child beforeMount
Child mounted
Parent mounted

更新阶段:
Parent beforeUpdate
Child beforeUpdate
Child updated
Parent updated

卸载阶段:
Parent beforeUnmount
Child beforeUnmount
Child unmounted
Parent unmounted

依赖注入

provide和inject

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
<!-- Ancestor.vue -->
<template>
<parent-component />
</template>

<script>
import { provide, ref, reactive } from 'vue'

export default {
setup() {
const name = ref('张三')
const state = reactive({ age: 18 })

// 提供数据
provide('name', name)
provide('state', state)

// 提供方法
const updateName = (newName) => {
name.value = newName
}
provide('updateName', updateName)

return {}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- Child.vue -->
<template>
<grand-child />
</template>

<script>
import { inject } from 'vue'

export default {
setup() {
// 注入数据
const name = inject('name')
const state = inject('state')

// 注入方法
const updateName = inject('updateName')

return { name, state, updateName }
}
}
</script>

注入默认值

1
2
3
4
5
// 第三个参数为默认值
const name = inject('name', '默认名称')

// 使用工厂函数提供默认值
const user = inject('user', () => ({ name: '匿名', age: 0 }))

Symbols作为注入Key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// symbols.js
import { provide, inject, InjectionKey } from 'vue'

export const MyServiceKey = Symbol('MyService')

// Ancestor.vue
import { MyServiceKey } from './symbols'

export default {
setup() {
const myService = { /* ... */ }
provide(MyServiceKey, myService)
}
}

// Child.vue
import { MyServiceKey } from './symbols'

export default {
setup() {
const myService = inject(MyServiceKey)
return { myService }
}
}

Teleport

基本用法

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
<!-- Modal.vue -->
<template>
<teleport to="body">
<div class="modal">
<div class="modal-content">
<slot></slot>
<button @click="$emit('close')">关闭</button>
</div>
</div>
</teleport>
</template>

<style scoped>
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}

.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>

条件渲染

1
2
3
4
5
6
<!-- 禁用Teleport -->
<teleport to="body" :disabled="!isShow">
<div v-if="isShow" class="modal">
<p>模态框内容</p>
</div>
</teleport>

多个Teleport到同一目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 多个teleport可以同时传送到同一目标 -->
<teleport to="#modals">
<div>第一个</div>
</teleport>

<teleport to="#modals">
<div>第二个</div>
</teleport>

<!-- 结果 -->
<div id="modals">
<div>第一个</div>
<div>第二个</div>
</div>

Suspense

异步组件加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- AsyncComponent.vue -->
<template>
<div>异步组件内容</div>
</template>

<script>
export default {
// 异步setup
async setup() {
const data = await fetchData()
return { data }
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
<!-- Parent.vue -->
<template>
<suspense>
<template #default>
<async-component />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</suspense>
</template>

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<suspense>
<template #default>
<async-component />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</suspense>
</template>

<script>
import { onErrorCaptured } from 'vue'

export default {
setup() {
onErrorCaptured((err) => {
console.error('捕获到错误:', err)
return false // 阻止错误继续传播
})
}
}
</script>

全局API变化

createApp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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.mount('#app')

全局API对比

Vue2 Global API Vue3 Global API
Vue.config app.config
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties
Vue.filter 已移除

全局配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const app = createApp(App)

// 配置
app.config

// 运行时编译
app.config.compilerOptions.delimiters = ['${', '}']

// 全局错误处理
app.config.errorHandler = (err, vm, info) => {
console.error('全局错误:', err)
console.error('错误信息:', info)
}

// 警告处理
app.config.warnHandler = (msg, vm, trace) => {
// 警告处理
}

// 性能分析
app.config.performance = true

// 全局属性
app.config.globalProperties.$http = axios

实例方法

1
2
3
4
5
6
7
8
9
10
11
12
// 挂载
app.mount('#app')

// 卸载
const app = createApp(App)
app.unmount()

// provide
app.provide('key', 'value')

// 组合式API
app.combinedFeatures()

新组件

Fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- Vue2 - 需要单一根元素 -->
<template>
<div class="container">
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>

<!-- Vue3 - 可以多根节点 -->
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>

<!-- 有条件的多根节点 -->
<template>
<header v-if="hasHeader">...</header>
<main>...</main>
<footer v-if="hasFooter">...</footer>
</template>

keep-alive变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Vue3 - 使用include/exclude -->
<keep-alive :include="['Home', 'About']" :exclude="['Contact']" :max="10">
<component :is="currentComponent" />
</keep-alive>

<!-- 结合Suspense -->
<keep-alive>
<suspense>
<template #default>
<async-component />
</template>
<template #fallback>
<div>加载中</div>
</template>
</suspense>
</keep-alive>

v-memo

1
2
3
4
5
6
7
8
9
10
11
<!-- 缓存模板子树 -->
<div v-memo="[valueA, valueB]">
<p>{{ valueA }}</p>
<p>{{ valueB }}</p>
<p>{{ valueC }}</p> <!-- valueC变化时也会更新 -->
</div>

<!-- 列表渲染优化 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.status]">
<p>{{ item.content }}</p>
</div>

总结

Composition API优势

  1. 更好的逻辑复用 - 通过函数组合实现逻辑复用
  2. 更好的类型推断 - setup函数的参数和返回值都可以类型化
  3. 更灵活的代码组织 - 相关逻辑可以放在一起
  4. 更好的Tree-shaking - 未使用的功能不会被打包

常用组合函数

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
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
const x = ref(0)
const y = ref(0)

const update = (e) => {
x.value = e.pageX
y.value = e.pageY
}

onMounted(() => {
window.addEventListener('mousemove', update)
})

onUnmounted(() => {
window.removeEventListener('mousemove', update)
})

return { x, y }
}

// 使用
import { useMouse } from './useMouse'

export default {
setup() {
const { x, y } = useMouse()
return { x, y }
}
}

相关链接