前言

Vue 3是Vue.js框架的第三个主要版本,于2020年9月正式发布。作为一个渐进式JavaScript框架,Vue 3在保持Vue 2核心理念的基础上,引入了许多革命性的改进和新特性。本文将深入探讨Vue 3的核心概念、新特性、最佳实践以及与Vue 2的主要区别,帮助开发者全面掌握这个现代化的前端框架。

一、Vue 3概述

(一)什么是Vue 3

Vue 3是一个用于构建用户界面的渐进式JavaScript框架。它专注于视图层,采用自底向上增量开发的设计理念。Vue 3的目标是通过尽可能简单的API实现响应式数据绑定和组合的视图组件。5

(二)Vue 3的核心特性

1. 性能提升

相比Vue 2,Vue 3在性能方面有了显著提升:2

  • 渲染性能:提升1.3~2倍
  • 打包体积:更小的bundle size
  • 内存使用:更高效的内存管理

2. Tree-shaking支持

Vue 3支持Tree-shaking,这意味着可以按需引入功能,未使用的代码不会被打包到最终的bundle中:3

1
2
3
4
5
// 按需引入Vue功能
import { createApp, ref, reactive } from 'vue'

// 只有使用到的功能会被打包
const app = createApp({})

3. 更好的TypeScript支持

Vue 3从底层重新设计,提供了更好的TypeScript支持,包括:

  • 更准确的类型推断
  • 更好的IDE支持
  • 原生TypeScript支持

(三)Vue 3的优势

  1. 响应式数据绑定:强大的响应式系统确保数据变化自动反映到视图
  2. 组件化开发:将应用分解为可复用的组件
  3. 渐进式框架:可以按需引入特性,适应不同规模的项目
  4. 简洁的模板语法:直观易学的模板语法
  5. 虚拟DOM:提高渲染性能
  6. 丰富的生态系统:活跃的社区和完善的工具链5

二、Composition API详解

(一)什么是Composition API

Composition API是Vue 3引入的一组新的API,用于组织和复用组件逻辑。与传统的Options API不同,Composition API通过函数式的方式将相关的逻辑组合在一起。2

(二)Composition API的优势

  1. 逻辑复用更便捷:通过组合函数可以轻松复用和共享逻辑
  2. 代码组织更清晰:将相关的状态和行为放在同一个函数中
  3. TypeScript更友好:提供更好的类型推断
  4. 解决大型项目维护问题:避免Options API在大型项目中的代码分散问题1

(三)setup()函数

setup()函数是Vue 3中专门为组件提供的新属性,它为使用Composition API提供了统一的入口:3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 基本的setup函数
export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)

// 插槽 (非响应式对象)
console.log(context.slots)

// 触发事件 (方法)
console.log(context.emit)

// 返回的数据和方法可以在模板中使用
return {
// 数据和方法
}
}
}

(四)响应式API

1. ref()

ref()用于创建响应式的基本数据类型:

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

export default {
setup() {
const count = ref(0)
const message = ref('Hello Vue 3')

const increment = () => {
count.value++
}

return {
count,
message,
increment
}
}
}

2. reactive()

reactive()用于创建响应式的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { reactive } from 'vue'

export default {
setup() {
const state = reactive({
name: 'Vue 3',
version: '3.0',
features: ['Composition API', 'Multiple root nodes']
})

const updateName = (newName) => {
state.name = newName
}

return {
state,
updateName
}
}
}

3. computed()

计算属性在Composition API中的使用:

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

export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})

return {
firstName,
lastName,
fullName
}
}
}

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

export default {
setup() {
const count = ref(0)
const message = ref('')

// watch
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`)
})

// watchEffect
watchEffect(() => {
console.log(`count is ${count.value}`)
})

return {
count,
message
}
}
}

(五)组合函数(Composables)

组合函数是利用Composition API封装和复用有状态逻辑的函数:2

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
// src/composables/useTodos.js
import { ref, computed } from 'vue'

export function useTodos() {
const todos = ref([
{ id: 1, text: '学习 Vue3', completed: false },
{ id: 2, text: '编写 Composition API 教程', completed: false }
])

const newTodo = ref('')

const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value,
completed: false
})
newTodo.value = ''
}
}

const removeTodo = (id) => {
todos.value = todos.value.filter(todo => todo.id !== id)
}

const completedCount = computed(() => {
return todos.value.filter(todo => todo.completed).length
})

const totalCount = computed(() => todos.value.length)

return {
todos,
newTodo,
addTodo,
removeTodo,
completedCount,
totalCount
}
}

在组件中使用组合函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// TodoApp.vue
import { useTodos } from '@/composables/useTodos'

export default {
setup() {
const {
todos,
newTodo,
addTodo,
removeTodo,
completedCount,
totalCount
} = useTodos()

return {
todos,
newTodo,
addTodo,
removeTodo,
completedCount,
totalCount
}
}
}

三、响应式系统深度解析

(一)Vue 3响应式系统原理

Vue 3使用Proxy API替代了Vue 2中的Object.defineProperty,实现了更强大的响应式系统。3

1. Proxy vs Object.defineProperty

Object.defineProperty的局限性:4

  • 无法检测对象属性的添加和删除
  • 数组API方法无法监听
  • 需要对每个属性进行遍历监听
  • 深层嵌套对象需要深层监听,造成性能问题

Proxy的优势:5

  • 可以监听整个对象,而不是单个属性
  • 支持数组索引和length属性的监听
  • 支持Map、Set、WeakMap、WeakSet等数据结构
  • 有13种拦截方法,功能更强大

2. 响应式实现原理

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
// 简化的响应式实现
function reactive(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj
}

// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)

// 依赖收集
track(target, key)

// 如果是对象,递归代理
return isObject(res) ? reactive(res) : res
},

set(target, key, value, receiver) {
const oldValue = target[key]
const res = Reflect.set(target, key, value, receiver)

if (oldValue !== value) {
console.log(`设置${key}:${value}`)
// 触发更新
trigger(target, key)
}

return res
},

deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)

// 触发更新
trigger(target, key)
return res
}
})

return observed
}

3. 依赖收集和触发机制

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
// 依赖收集和触发的简化实现
const targetMap = new WeakMap()
let activeEffect = null

function track(target, key) {
if (!activeEffect) return

let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}

let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}

dep.add(activeEffect)
}

function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return

const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}

function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}

(二)ref vs reactive

1. ref的实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}

2. 使用场景对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ref适用于基本数据类型
const count = ref(0)
const message = ref('hello')
const isVisible = ref(true)

// reactive适用于对象和数组
const state = reactive({
user: {
name: 'John',
age: 30
},
todos: []
})

// 访问方式不同
console.log(count.value) // ref需要.value
console.log(state.user.name) // reactive直接访问

3. 响应式转换工具

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

const state = reactive({
name: 'Vue',
version: 3
})

// toRef: 将reactive对象的属性转为ref
const name = toRef(state, 'name')

// toRefs: 将reactive对象的所有属性转为ref
const { name: nameRef, version: versionRef } = toRefs(state)

// unref: 获取ref的值,如果不是ref则直接返回
const value = unref(name) // 等同于 isRef(name) ? name.value : name

// 类型检查
console.log(isRef(name)) // true
console.log(isReactive(state)) // true

(三)Vue 3.2响应式优化

Vue 3.2对响应式系统进行了进一步优化:1

  1. 更高效的ref实现:读取性能提升260%,写入性能提升50%
  2. 位标记优化:使用位运算优化依赖追踪
  3. effect嵌套优化:更好地处理effect嵌套场景

四、新增组件和特性

(一)Fragment(片段)

Vue 3支持组件有多个根节点,不再需要单一根元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Vue 2中必须有单一根元素 -->
<template>
<div>
<header>Header</header>
<main>Main content</main>
<footer>Footer</footer>
</div>
</template>

<!-- Vue 3中可以有多个根节点 -->
<template>
<header>Header</header>
<main>Main content</main>
<footer>Footer</footer>
</template>

(二)Teleport(传送门)

Teleport允许我们将组件的一部分模板”传送”到DOM中的其他位置:

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
48
49
50
<template>
<div class="modal-container">
<button @click="showModal = true">打开模态框</button>

<!-- 将模态框传送到body下 -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<div class="modal-content">
<h3>模态框标题</h3>
<p>模态框内容</p>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</div>
</template>

<script>
import { ref } from 'vue'

export default {
setup() {
const showModal = ref(false)

return {
showModal
}
}
}
</script>

<style>
.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>

(三)Suspense(悬念)

Suspense让组件在渲染之前进行”等待”,并在等待时显示fallback内容:3

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
<template>
<Suspense>
<!-- 异步组件 -->
<template #default>
<AsyncComponent />
</template>

<!-- 加载中的fallback内容 -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue'

// 定义异步组件
const AsyncComponent = defineAsyncComponent(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
template: '<div>异步组件加载完成!</div>'
})
}, 2000)
})
})

export default {
components: {
AsyncComponent
}
}
</script>

异步组件的高级用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { defineAsyncComponent } from 'vue'

// 带选项的异步组件
const AsyncComponent = defineAsyncComponent({
// 工厂函数
loader: () => import('./AsyncComponent.vue'),

// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,

// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,

// 加载失败后展示的组件
errorComponent: ErrorComponent,

// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})

五、Vue 3与Vue 2的主要区别

(一)API设计变化

1. 全局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
// Vue 2
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false
Vue.use(SomePlugin)
Vue.mixin(SomeMixin)
Vue.component('GlobalComponent', SomeComponent)

new Vue({
render: h => h(App)
}).$mount('#app')

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

const app = createApp(App)

app.config.globalProperties.customProperty = 'custom value'
app.use(SomePlugin)
app.mixin(SomeMixin)
app.component('GlobalComponent', SomeComponent)

app.mount('#app')

2. 组件定义变化

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
48
49
// Vue 2 Options API
export default {
data() {
return {
count: 0,
message: 'Hello'
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('Component mounted')
}
}

// Vue 3 Composition API
import { ref, computed, onMounted } from 'vue'

export default {
setup() {
const count = ref(0)
const message = ref('Hello')

const doubleCount = computed(() => count.value * 2)

const increment = () => {
count.value++
}

onMounted(() => {
console.log('Component mounted')
})

return {
count,
message,
doubleCount,
increment
}
}
}

(二)生命周期变化

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
48
49
50
51
52
// Vue 2生命周期
export default {
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeDestroy() {},
destroyed() {}
}

// Vue 3 Composition API生命周期
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'

export default {
setup() {
// 注意:没有beforeCreate和created对应的组合式API
// 因为setup()就相当于这两个钩子

onBeforeMount(() => {
console.log('Before mount')
})

onMounted(() => {
console.log('Mounted')
})

onBeforeUpdate(() => {
console.log('Before update')
})

onUpdated(() => {
console.log('Updated')
})

onBeforeUnmount(() => {
console.log('Before unmount')
})

onUnmounted(() => {
console.log('Unmounted')
})
}
}

(三)v-model变化

Vue 3中的v-model有了重大改变:3

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
// Vue 2中的v-model
// 父组件
<CustomInput v-model="searchText" />

// 子组件
export default {
props: ['value'],
methods: {
updateValue(value) {
this.$emit('input', value)
}
}
}

// Vue 3中的v-model
// 父组件
<CustomInput v-model="searchText" />

// 子组件
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
methods: {
updateValue(value) {
this.$emit('update:modelValue', value)
}
}
}

// Vue 3支持多个v-model
<UserName
v-model:first-name="firstName"
v-model:last-name="lastName"
/>

(四)移除的特性

  1. 过滤器(Filters):Vue 3中移除了过滤器,建议使用计算属性或方法替代
  2. $children:移除了$children属性
  3. 事件API:移除了$on、$off、$once等事件API
  4. 内联模板:移除了inline-template特性

六、Vue 3生态系统

(一)官方库

1. Vue Router 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 安装
npm install vue-router@4

// 使用
import { createRouter, createWebHistory } from 'vue-router'
import Home from './components/Home.vue'
import About from './components/About.vue'

const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]

const router = createRouter({
history: createWebHistory(),
routes
})

export default router

2. Pinia(状态管理)

Pinia是Vue 3推荐的状态管理库,替代Vuex:

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
// 安装
npm install pinia

// 定义store
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),

getters: {
doubleCount: (state) => state.count * 2
},

actions: {
increment() {
this.count++
}
}
})

// 在组件中使用
import { useCounterStore } from '@/stores/counter'

export default {
setup() {
const counter = useCounterStore()

return {
counter
}
}
}

3. Vue DevTools

Vue 3专用的开发者工具,支持:

  • Composition API调试
  • 时间旅行调试
  • 组件检查器
  • 性能分析

(二)构建工具

1. Vite

Vite是Vue 3推荐的构建工具:3

1
2
3
4
5
# 创建Vue 3项目
npm create vite@latest my-vue-app -- --template vue

# 或者使用TypeScript模板
npm create vite@latest my-vue-app -- --template vue-ts

Vite的特点:

  • 极快的冷启动
  • 即时的模块热更新
  • 真正的按需编译
  • 丰富的插件生态

2. Vite配置示例

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
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
plugins: [vue()],

resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},

server: {
port: 3000,
open: true
},

build: {
outDir: 'dist',
sourcemap: true,

rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
utils: ['lodash', 'axios']
}
}
}
}
})

七、Vue 3最佳实践

(一)组件设计原则

1. 单一职责原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 好的做法:职责单一的组件 -->
<template>
<div class="user-card">
<img :src="user.avatar" :alt="user.name" />
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
</template>

<script>
export default {
props: {
user: {
type: Object,
required: true
}
}
}
</script>

2. Props验证

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
export default {
props: {
// 基础类型检查
title: String,

// 多种可能的类型
count: [Number, String],

// 必填的字符串
message: {
type: String,
required: true
},

// 带有默认值的数字
size: {
type: Number,
default: 100
},

// 带有默认值的对象
user: {
type: Object,
default: () => ({
name: 'Guest',
email: ''
})
},

// 自定义验证函数
status: {
type: String,
validator: (value) => {
return ['pending', 'success', 'error'].includes(value)
}
}
}
}

3. 事件命名规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<button @click="handleClick">点击我</button>
</template>

<script>
export default {
emits: {
// 验证事件
'user-updated': (payload) => {
return payload && typeof payload.id === 'number'
},

// 简单声明
'item-selected': null
},

methods: {
handleClick() {
// 使用kebab-case命名事件
this.$emit('user-updated', { id: 1, name: 'John' })
}
}
}
</script>

(二)Composition API最佳实践

1. 逻辑分组

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
// 好的做法:按功能分组
export default {
setup() {
// 用户相关逻辑
const { user, updateUser, fetchUser } = useUser()

// 搜索相关逻辑
const { searchQuery, searchResults, performSearch } = useSearch()

// 分页相关逻辑
const { currentPage, totalPages, changePage } = usePagination()

return {
// 用户
user,
updateUser,
fetchUser,

// 搜索
searchQuery,
searchResults,
performSearch,

// 分页
currentPage,
totalPages,
changePage
}
}
}

2. 组合函数设计

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
48
49
50
51
52
53
54
// composables/useApi.js
import { ref, reactive } from 'vue'
import axios from 'axios'

export function useApi(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)

const execute = async (config = {}) => {
try {
loading.value = true
error.value = null

const response = await axios({
url,
...config
})

data.value = response.data
return response.data
} catch (err) {
error.value = err
throw err
} finally {
loading.value = false
}
}

return {
data: readonly(data),
error: readonly(error),
loading: readonly(loading),
execute
}
}

// 使用
export default {
setup() {
const { data: users, loading, error, execute } = useApi('/api/users')

const fetchUsers = () => execute({ method: 'GET' })

onMounted(fetchUsers)

return {
users,
loading,
error,
fetchUsers
}
}
}

3. 响应式数据管理

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
// 好的做法:合理使用ref和reactive
export default {
setup() {
// 基本类型使用ref
const count = ref(0)
const message = ref('')
const isVisible = ref(true)

// 对象使用reactive
const form = reactive({
name: '',
email: '',
age: 0
})

// 数组使用reactive
const items = reactive([])

// 避免解构reactive对象
// 错误做法
// const { name, email } = form // 失去响应性

// 正确做法
const { name, email } = toRefs(form)

return {
count,
message,
isVisible,
form,
items,
name,
email
}
}
}

(三)性能优化

1. 组件懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 路由懒加载
const routes = [
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import('@/views/About.vue')
}
]

// 组件懒加载
import { defineAsyncComponent } from 'vue'

export default {
components: {
HeavyComponent: defineAsyncComponent(() =>
import('@/components/HeavyComponent.vue')
)
}
}

2. 使用v-memo优化列表渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<!-- 使用v-memo缓存列表项 -->
<div
v-for="item in list"
:key="item.id"
v-memo="[item.id, item.name, item.status]"
>
<span>{{ item.name }}</span>
<span>{{ item.status }}</span>
</div>
</div>
</template>

3. 合理使用计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default {
setup() {
const items = ref([])
const filter = ref('')

// 使用计算属性缓存过滤结果
const filteredItems = computed(() => {
if (!filter.value) return items.value

return items.value.filter(item =>
item.name.toLowerCase().includes(filter.value.toLowerCase())
)
})

return {
items,
filter,
filteredItems
}
}
}

八、Vue 3项目实战

(一)项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── common/ # 通用组件
│ └── ui/ # UI组件
├── composables/ # 组合函数
├── directives/ # 自定义指令
├── plugins/ # 插件
├── router/ # 路由配置
├── stores/ # 状态管理
├── utils/ # 工具函数
├── views/ # 页面组件
├── App.vue # 根组件
└── main.js # 入口文件

(二)实战示例:Todo应用

1. 创建Todo Store

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// stores/todos.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTodosStore = defineStore('todos', () => {
const todos = ref([])
const filter = ref('all') // all, active, completed

const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed)
case 'completed':
return todos.value.filter(todo => todo.completed)
default:
return todos.value
}
})

const activeTodosCount = computed(() => {
return todos.value.filter(todo => !todo.completed).length
})

const addTodo = (text) => {
todos.value.push({
id: Date.now(),
text,
completed: false,
createdAt: new Date()
})
}

const removeTodo = (id) => {
const index = todos.value.findIndex(todo => todo.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}

const toggleTodo = (id) => {
const todo = todos.value.find(todo => todo.id === id)
if (todo) {
todo.completed = !todo.completed
}
}

const clearCompleted = () => {
todos.value = todos.value.filter(todo => !todo.completed)
}

const setFilter = (newFilter) => {
filter.value = newFilter
}

return {
todos,
filter,
filteredTodos,
activeTodosCount,
addTodo,
removeTodo,
toggleTodo,
clearCompleted,
setFilter
}
})

2. Todo组件

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<!-- components/TodoItem.vue -->
<template>
<li class="todo-item" :class="{ completed: todo.completed }">
<input
type="checkbox"
:checked="todo.completed"
@change="$emit('toggle', todo.id)"
/>

<span
v-if="!editing"
class="todo-text"
@dblclick="startEdit"
>
{{ todo.text }}
</span>

<input
v-else
ref="editInput"
v-model="editText"
class="edit-input"
@blur="finishEdit"
@keyup.enter="finishEdit"
@keyup.esc="cancelEdit"
/>

<button
class="delete-btn"
@click="$emit('remove', todo.id)"
>
×
</button>
</li>
</template>

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

export default {
props: {
todo: {
type: Object,
required: true
}
},

emits: ['toggle', 'remove', 'update'],

setup(props, { emit }) {
const editing = ref(false)
const editText = ref('')
const editInput = ref(null)

const startEdit = () => {
editing.value = true
editText.value = props.todo.text

nextTick(() => {
editInput.value?.focus()
})
}

const finishEdit = () => {
if (editing.value) {
const text = editText.value.trim()
if (text) {
emit('update', props.todo.id, text)
} else {
emit('remove', props.todo.id)
}
editing.value = false
}
}

const cancelEdit = () => {
editing.value = false
editText.value = props.todo.text
}

return {
editing,
editText,
editInput,
startEdit,
finishEdit,
cancelEdit
}
}
}
</script>

<style scoped>
.todo-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}

.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}

.todo-text {
flex: 1;
margin: 0 10px;
cursor: pointer;
}

.edit-input {
flex: 1;
margin: 0 10px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}

.delete-btn {
background: #ff4757;
color: white;
border: none;
border-radius: 3px;
padding: 5px 10px;
cursor: pointer;
}

.delete-btn:hover {
background: #ff3838;
}
</style>

3. 主应用组件

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
<!-- App.vue -->
<template>
<div class="todo-app">
<header class="header">
<h1>Todo App</h1>
<input
v-model="newTodo"
class="new-todo"
placeholder="What needs to be done?"
@keyup.enter="addTodo"
/>
</header>

<main class="main" v-if="todos.length">
<ul class="todo-list">
<TodoItem
v-for="todo in filteredTodos"
:key="todo.id"
:todo="todo"
@toggle="toggleTodo"
@remove="removeTodo"
@update="updateTodo"
/>
</ul>
</main>

<footer class="footer" v-if="todos.length">
<span class="todo-count">
{{ activeTodosCount }} item{{ activeTodosCount !== 1 ? 's' : '' }} left
</span>

<div class="filters">
<button
v-for="filterName in ['all', 'active', 'completed']"
:key="filterName"
:class="{ active: filter === filterName }"
@click="setFilter(filterName)"
>
{{ filterName }}
</button>
</div>

<button
v-if="todos.length > activeTodosCount"
class="clear-completed"
@click="clearCompleted"
>
Clear completed
</button>
</footer>
</div>
</template>

<script>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useTodosStore } from '@/stores/todos'
import TodoItem from '@/components/TodoItem.vue'

export default {
components: {
TodoItem
},

setup() {
const todosStore = useTodosStore()
const newTodo = ref('')

const {
todos,
filter,
filteredTodos,
activeTodosCount
} = storeToRefs(todosStore)

const {
addTodo: addTodoToStore,
removeTodo,
toggleTodo,
clearCompleted,
setFilter
} = todosStore

const addTodo = () => {
const text = newTodo.value.trim()
if (text) {
addTodoToStore(text)
newTodo.value = ''
}
}

const updateTodo = (id, text) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.text = text
}
}

return {
newTodo,
todos,
filter,
filteredTodos,
activeTodosCount,
addTodo,
removeTodo,
toggleTodo,
updateTodo,
clearCompleted,
setFilter
}
}
}
</script>

<style>
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}

.header h1 {
text-align: center;
color: #333;
margin-bottom: 20px;
}

.new-todo {
width: 100%;
padding: 15px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 20px;
}

.todo-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #ddd;
border-radius: 5px;
}

.footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
margin-top: 20px;
}

.filters button {
margin: 0 5px;
padding: 5px 10px;
border: 1px solid #ddd;
background: white;
border-radius: 3px;
cursor: pointer;
}

.filters button.active {
background: #007bff;
color: white;
}

.clear-completed {
padding: 5px 10px;
border: 1px solid #ddd;
background: white;
border-radius: 3px;
cursor: pointer;
}

.clear-completed:hover {
background: #f8f9fa;
}
</style>

九、构建与部署

(一)Vite构建配置

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
plugins: [vue()],

resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},

build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,

// 代码分割
rollupOptions: {
output: {
manualChunks: {
// 将Vue相关库打包到vendor chunk
vendor: ['vue', 'vue-router', 'pinia'],

// 将UI库单独打包
ui: ['element-plus'],

// 将工具库单独打包
utils: ['lodash', 'axios', 'dayjs']
}
}
},

// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},

// 开发服务器配置
server: {
port: 3000,
open: true,
cors: true,

// 代理配置
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})

(二)性能优化配置

1. 静态资源优化

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
48
49
50
51
52
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer'
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
plugins: [
vue(),

// 打包分析
visualizer({
filename: 'dist/stats.html',
open: true
}),

// Gzip压缩
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz'
})
],

build: {
rollupOptions: {
output: {
// 静态资源分类
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.')
let extType = info[info.length - 1]

if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'media'
} else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'img'
} else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'fonts'
}

return `assets/${extType}/[name]-[hash][extname]`
},

// JS文件分类
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js'
}
}
}
})

2. CDN配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
plugins: [vue()],

build: {
rollupOptions: {
external: ['vue', 'vue-router', 'pinia'],
output: {
globals: {
vue: 'Vue',
'vue-router': 'VueRouter',
pinia: 'Pinia'
}
}
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 3 App</title>

<!-- CDN引入 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.js"></script>
<script src="https://unpkg.com/pinia@2/dist/pinia.iife.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

(三)部署配置

1. Nginx配置

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
server {
listen 80;
server_name your-domain.com;
root /var/www/vue-app/dist;
index index.html;

# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;

# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

# HTML文件不缓存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}

# SPA路由支持
location / {
try_files $uri $uri/ /index.html;
}

# API代理
location /api/ {
proxy_pass http://backend-server:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

2. Docker部署

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
# Dockerfile
# 构建阶段
FROM node:18-alpine as build-stage

WORKDIR /app

# 复制package文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 生产阶段
FROM nginx:alpine as production-stage

# 复制构建结果
COPY --from=build-stage /app/dist /usr/share/nginx/html

# 复制nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
1
2
3
4
5
6
7
8
9
10
11
# docker-compose.yml
version: '3.8'

services:
vue-app:
build: .
ports:
- "80:80"
environment:
- NODE_ENV=production
restart: unless-stopped

十、性能优化实践

(一)代码层面优化

1. 使用shallowRef和shallowReactive

对于大型数据结构,使用浅层响应式可以提高性能:

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

export default {
setup() {
// 对于大型数据结构,使用shallowRef
const largeData = shallowRef({
items: new Array(10000).fill(0).map((_, i) => ({ id: i, name: `Item ${i}` }))
})

// 普通ref会深度监听,性能较差
// const largeData = ref({ items: [...] }) // 避免这样做

// 更新数据时,需要替换整个对象
const updateData = () => {
largeData.value = {
items: largeData.value.items.concat({ id: Date.now(), name: 'New Item' })
}
}

return {
largeData,
updateData
}
}
}

2. 使用v-once和v-memo优化静态内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<!-- 静态内容使用v-once -->
<h1 v-once>{{ title }}</h1>

<!-- 使用v-memo缓存复杂计算 -->
<div v-memo="[user.id, user.name]">
<UserProfile :user="user" />
</div>

<!-- 列表优化 -->
<div
v-for="item in items"
:key="item.id"
v-memo="[item.id, item.status, item.priority]"
>
<ExpensiveComponent :item="item" />
</div>
</div>
</template>

3. 异步组件和代码分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 路由级别的代码分割
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue')
},
{
path: '/profile',
component: () => import('@/views/Profile.vue')
}
]

// 组件级别的懒加载
import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent({
loader: () => import('@/components/HeavyChart.vue'),
loadingComponent: () => '<div>Loading chart...</div>',
errorComponent: () => '<div>Failed to load chart</div>',
delay: 200,
timeout: 3000
})

(二)构建优化

1. Tree Shaking优化

1
2
3
4
5
6
7
8
9
// 按需引入工具库
import { debounce, throttle } from 'lodash-es'
// 而不是
// import _ from 'lodash'

// 按需引入UI库
import { ElButton, ElInput } from 'element-plus'
// 而不是
// import ElementPlus from 'element-plus'

2. 预加载和预获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// 关键路径组件
critical: ['@/views/Home.vue', '@/views/Login.vue'],

// 次要组件
secondary: ['@/views/Profile.vue', '@/views/Settings.vue']
}
}
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 在路由组件中预加载 -->
<template>
<div>
<router-link
to="/profile"
@mouseenter="preloadProfile"
>
Profile
</router-link>
</div>
</template>

<script>
export default {
methods: {
preloadProfile() {
// 预加载Profile组件
import('@/views/Profile.vue')
}
}
}
</script>

(三)运行时优化

1. 虚拟滚动

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<!-- VirtualList.vue -->
<template>
<div
ref="container"
class="virtual-list"
@scroll="handleScroll"
>
<div
class="virtual-list-phantom"
:style="{ height: totalHeight + 'px' }"
></div>

<div
class="virtual-list-content"
:style="{ transform: `translateY(${offset}px)` }"
>
<div
v-for="item in visibleItems"
:key="item.id"
class="virtual-list-item"
:style="{ height: itemHeight + 'px' }"
>
<slot :item="item"></slot>
</div>
</div>
</div>
</template>

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

export default {
props: {
items: Array,
itemHeight: {
type: Number,
default: 50
},
visibleCount: {
type: Number,
default: 10
}
},

setup(props) {
const container = ref(null)
const scrollTop = ref(0)

const totalHeight = computed(() => {
return props.items.length * props.itemHeight
})

const startIndex = computed(() => {
return Math.floor(scrollTop.value / props.itemHeight)
})

const endIndex = computed(() => {
return Math.min(
startIndex.value + props.visibleCount,
props.items.length
)
})

const visibleItems = computed(() => {
return props.items.slice(startIndex.value, endIndex.value)
})

const offset = computed(() => {
return startIndex.value * props.itemHeight
})

const handleScroll = (e) => {
scrollTop.value = e.target.scrollTop
}

return {
container,
totalHeight,
visibleItems,
offset,
handleScroll
}
}
}
</script>

2. 防抖和节流

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
// composables/useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)

watch(value, (newValue) => {
const timer = setTimeout(() => {
debouncedValue.value = newValue
}, delay)

return () => clearTimeout(timer)
})

return debouncedValue
}

// 使用
export default {
setup() {
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 500)

watch(debouncedQuery, (newQuery) => {
// 执行搜索
performSearch(newQuery)
})

return {
searchQuery
}
}
}

十一、总结

Vue 3作为Vue.js框架的重大升级版本,带来了许多革命性的改进和新特性。通过本文的深入探讨,我们可以看到Vue 3在以下几个方面的显著优势:

(一)核心特性总结

  1. Composition API:提供了更灵活的逻辑组织方式,解决了大型项目中代码复用和维护的问题
  2. 响应式系统优化:基于Proxy的实现提供了更强大的响应式能力和更好的性能
  3. 更好的TypeScript支持:从底层设计就考虑了TypeScript集成,提供更准确的类型推断
  4. Tree-shaking友好:支持按需引入,显著减小打包体积
  5. Fragment支持:允许组件有多个根节点,提供更灵活的模板结构
  6. 新增组件:Teleport、Suspense等新组件解决了特定场景下的开发需求

(二)开发建议

  1. 渐进式迁移:对于现有Vue 2项目,可以逐步引入Vue 3特性,不必一次性重写
  2. 合理使用Composition API:在复杂组件和需要逻辑复用的场景下使用,简单组件仍可使用Options API
  3. 性能优化:充分利用Vue 3的性能优化特性,如shallowRef、v-memo等
  4. 状态管理:推荐使用Pinia替代Vuex,享受更好的TypeScript支持和开发体验
  5. 构建工具:使用Vite作为构建工具,获得更快的开发体验

(三)学习路径建议

  1. 基础阶段:掌握Vue 3基本概念、响应式系统、组件开发
  2. 进阶阶段:深入学习Composition API、自定义组合函数、高级组件模式
  3. 实战阶段:结合Vue Router 4、Pinia等生态库开发完整项目
  4. 优化阶段:学习性能优化技巧、构建优化、部署最佳实践

Vue 3不仅保持了Vue.js一贯的简洁易学特点,还在性能、开发体验和生态系统方面都有了显著提升。随着生态系统的不断完善,Vue 3必将成为现代前端开发的重要选择。

参考资料