Appearance
Vue 3 新特性详解
一、Vue 3 概述
Vue 3 是 Vue.js 的最新主版本,于 2020 年 9 月发布。相比 Vue 2,Vue 3 在性能、可维护性、开发体验等方面都有显著提升。
核心改进
| 改进领域 | 具体内容 |
|---|---|
| 性能提升 | 虚拟 DOM 重写、diff算法优化、编译器优化、响应式系统改进 |
| 组合式 API | 更灵活的代码组织方式,更好的逻辑复用 |
| TypeScript 支持 | 源码使用 TypeScript 重写,类型推断更完善 |
| Tree-shaking | 按需引入,减小打包体积 |
| 新内置组件 | Teleport、Suspense、Fragment |
| 自定义渲染器 | 轻松创建自定义渲染器 |
二、Composition API(组合式 API)
Composition API 是 Vue 3 最重要的新特性,提供了一种全新的组织组件逻辑的方式。
1. 与 Options API 对比
Options API(Vue 2 风格)
vue
<template>
<div>
<p>计数器: {{ count }}</p>
<p>双倍: {{ doubled }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
computed: {
doubled() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('组件已挂载')
}
}
</script>Composition API(Vue 3 风格)
vue
<template>
<div>
<p>计数器: {{ count }}</p>
<p>双倍: {{ doubled }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('组件已挂载')
})
</script>2. 响应式 API
ref - 基本类型响应式
vue
<script setup>
import { ref } from 'vue'
// 基本类型
const count = ref(0)
const message = ref('Hello')
const isActive = ref(true)
// 对象类型(内部会自动调用 reactive)
const user = ref({ name: '张三', age: 25 })
// 访问和修改
console.log(count.value) // 0
count.value++ // 修改
console.log(user.value.name) // 张三
</script>
<template>
<div>
<p>{{ count }}</p>
<p>{{ message }}</p>
<p>{{ user.name }}</p>
</div>
</template>使用场景:
- 管理基本类型数据(数字、字符串、布尔值)
- 需要替换整个对象的场景
- 需要明确区分响应式值和普通值的场景
reactive - 对象类型响应式
vue
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: '张三',
profile: {
email: 'zhangsan@example.com'
}
},
items: ['a', 'b', 'c']
})
// 直接访问,无需 .value
console.log(state.count)
state.count++
// 嵌套对象也是响应式的
state.user.profile.email = 'new@example.com'
// 数组操作也是响应式的
state.items.push('d')
</script>
<template>
<div>
<p>{{ state.count }}</p>
<p>{{ state.user.name }}</p>
<ul>
<li v-for="item in state.items" :key="item">{{ item }}</li>
</ul>
</div>
</template>使用场景:
- 管理复杂对象状态
- 需要深度响应式的场景
- 组件内部状态管理
ref vs reactive 对比
vue
<script setup>
import { ref, reactive, isRef, isReactive, toRef, toRefs } from 'vue'
// ref 特点
const countRef = ref(0)
countRef.value = 1 // 需要 .value
// reactive 特点
const stateReactive = reactive({ count: 0 })
stateReactive.count = 1 // 不需要 .value
// 解构响应式对象会失去响应性
const { count } = stateReactive // count 不再是响应式的!
// 使用 toRefs 保持响应性
const { count: reactiveCount } = toRefs(stateReactive)
reactiveCount.value = 2 // 保持响应性
// 使用 toRef 创建单个属性的 ref
const countRef2 = toRef(stateReactive, 'count')
</script>| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 任意类型 | 对象类型 |
| 访问方式 | 需要 .value | 直接访问 |
| 解构 | 保持响应性 | 失去响应性(需用 toRefs) |
| 重新赋值 | 可以整体替换 | 不能整体替换 |
| TypeScript | 类型推断更简单 | 需要类型标注 |
3. computed - 计算属性
vue
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 只读计算属性
const fullName = computed(() => `${firstName.value}${lastName.value}`)
// 可写计算属性
const fullNameWritable = computed({
get() {
return `${firstName.value}${lastName.value}`
},
set(newValue) {
firstName.value = newValue.charAt(0)
lastName.value = newValue.slice(1)
}
})
// 复杂计算
const items = ref([
{ id: 1, name: '商品A', price: 100, quantity: 2 },
{ id: 2, name: '商品B', price: 200, quantity: 1 }
])
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
const expensiveItems = computed(() => {
return items.value.filter(item => item.price > 150)
})
</script>
<template>
<div>
<p>姓名: {{ fullName }}</p>
<p>总价: {{ totalPrice }}</p>
<p>高价商品: {{ expensiveItems.length }} 件</p>
</div>
</template>使用场景:
- 派生状态计算
- 数据过滤和排序
- 格式化显示数据
- 缓存昂贵计算结果
4. watch 与 watchEffect
watch - 明确指定监听源
vue
<script setup>
import { ref, watch, reactive } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三', age: 25 })
// 监听 ref
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})
// 监听 reactive 属性
watch(
() => user.age,
(newAge, oldAge) => {
console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
}
)
// 监听多个源
const firstName = ref('张')
const lastName = ref('三')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`姓名从 ${oldFirst}${oldLast} 变为 ${newFirst}${newLast}`)
})
// 深度监听
const deepObject = ref({ nested: { value: 1 } })
watch(
deepObject,
newValue => {
console.log('对象发生变化:', newValue)
},
{ deep: true }
)
// 立即执行
watch(
count,
newValue => {
console.log('当前值:', newValue)
},
{ immediate: true }
)
// 监听对象内部变化
watch(
() => ({ ...user }),
newUser => {
console.log('用户信息变化:', newUser)
}
)
</script>watchEffect - 自动追踪依赖
vue
<script setup>
import { ref, watchEffect, onScopeDispose } from 'vue'
const count = ref(0)
const multiplier = ref(2)
// 自动追踪所有使用的响应式依赖
const stop = watchEffect(onCleanup => {
console.log(`结果: ${count.value * multiplier.value}`)
// 清理函数(下次执行前调用)
onCleanup(() => {
console.log('清理副作用')
})
})
// 停止监听
// stop()
// 带副作用的异步操作
const data = ref(null)
const loading = ref(false)
watchEffect(async onCleanup => {
loading.value = true
const controller = new AbortController()
onCleanup(() => controller.abort())
try {
const response = await fetch('/api/data', {
signal: controller.signal
})
data.value = await response.json()
} catch (e) {
if (e.name !== 'AbortError') {
console.error(e)
}
} finally {
loading.value = false
}
})
</script>watch vs watchEffect 对比:
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖追踪 | 手动指定 | 自动追踪 |
| 获取旧值 | 可以 | 不可以 |
| 懒执行 | 默认懒执行 | 立即执行 |
| 使用场景 | 需要精确控制监听源 | 自动收集依赖 |
5. 生命周期钩子
vue
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured
} from 'vue'
onBeforeMount(() => {
console.log('组件挂载前')
})
onMounted(() => {
console.log('组件已挂载,DOM 可访问')
// 初始化第三方库、DOM 操作、发起请求
})
onBeforeUpdate(() => {
console.log('组件更新前')
})
onUpdated(() => {
console.log('组件已更新')
// 注意:避免在此修改响应式状态,可能导致无限循环
})
onBeforeUnmount(() => {
console.log('组件卸载前')
// 清理定时器、取消订阅等
})
onUnmounted(() => {
console.log('组件已卸载')
})
// KeepAlive 相关
onActivated(() => {
console.log('组件被激活')
})
onDeactivated(() => {
console.log('组件被停用')
})
// 错误捕获
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err)
return false // 阻止错误继续传播
})
</script>生命周期映射表:
| Options API | Composition API |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeDestroy | onBeforeUnmount |
| destroyed | onUnmounted |
| activated | onActivated |
| deactivated | onDeactivated |
| errorCaptured | onErrorCaptured |
6. 依赖注入 (provide/inject)
vue
<!-- 祖先组件 -->
<script setup>
import { provide, ref, readonly } from 'vue'
const theme = ref('dark')
const user = ref({ name: '张三' })
// 提供只读数据
provide('theme', readonly(theme))
// 提供可修改数据(通过方法)
provide('updateTheme', newTheme => {
theme.value = newTheme
})
// 提供响应式对象
provide('user', user)
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
// 注入数据
const theme = inject('theme', 'light') // 第二个参数是默认值
const updateTheme = inject('updateTheme')
const user = inject('user')
// 使用 Symbol 作为 key(推荐)
const ThemeKey = Symbol('theme')
provide(ThemeKey, theme)
</script>最佳实践 - 使用 Symbol 和类型:
typescript
// keys.ts
import type { InjectionKey, Ref } from 'vue'
export interface Theme {
mode: 'light' | 'dark'
toggle: () => void
}
export const ThemeKey: InjectionKey<Theme> = Symbol('theme')
// 祖先组件
import { provide, ref } from 'vue'
import { ThemeKey } from './keys'
const mode = ref<'light' | 'dark'>('light')
provide(ThemeKey, {
mode: mode.value,
toggle: () => {
mode.value = mode.value === 'light' ? 'dark' : 'light'
}
})
// 后代组件
import { inject } from 'vue'
import { ThemeKey } from './keys'
const theme = inject(ThemeKey)!使用场景:
- 深层嵌套组件通信
- 主题配置传递
- 国际化设置
- 用户认证状态
7. 组合式函数 (Composables)
组合式函数是 Vue 3 中复用逻辑的核心方式。
基础示例
typescript
// composables/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
doubled,
increment,
decrement,
reset
}
}
// 使用
<script setup>
import { useCounter } from './composables/useCounter'
const { count, doubled, increment, reset } = useCounter(10)
</script>实用组合式函数示例
typescript
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// composables/useFetch.ts
import { ref, watchEffect, toValue } from 'vue'
import type { Ref } from 'vue'
export function useFetch<T>(url: Ref<string> | string) {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<Error | null>(null)
const loading = ref(false)
async function fetchData() {
loading.value = true
error.value = null
try {
const response = await fetch(toValue(url))
if (!response.ok) throw new Error(response.statusText)
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
watchEffect(() => {
fetchData()
})
return { data, error, loading, refetch: fetchData }
}
// composables/useLocalStorage.ts
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, defaultValue: T) {
const stored = localStorage.getItem(key)
const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)
watch(
data,
newValue => {
localStorage.setItem(key, JSON.stringify(newValue))
},
{ deep: true }
)
return data
}
// composables/useDebounce.ts
import { ref, watch } from 'vue'
export function useDebounce<T>(value: Ref<T>, delay = 300) {
const debouncedValue = ref(value.value) as Ref<T>
let timeout: ReturnType<typeof setTimeout>
watch(value, newValue => {
clearTimeout(timeout)
timeout = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
return debouncedValue
}
// composables/useToggle.ts
import { ref } from 'vue'
export function useToggle(initialValue = false) {
const value = ref(initialValue)
function toggle() {
value.value = !value.value
}
function setTrue() {
value.value = true
}
function setFalse() {
value.value = false
}
return { value, toggle, setTrue, setFalse }
}使用示例
vue
<script setup>
import { useMouse } from './composables/useMouse'
import { useFetch } from './composables/useFetch'
import { useLocalStorage } from './composables/useLocalStorage'
import { useToggle } from './composables/useToggle'
const { x, y } = useMouse()
const { data, loading, error } = useFetch('/api/users')
const theme = useLocalStorage('theme', 'light')
const { value: isOpen, toggle } = useToggle()
</script>
<template>
<div>
<p>鼠标位置: {{ x }}, {{ y }}</p>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error.message }}</div>
<div v-else>{{ data }}</div>
<button @click="toggle">{{ isOpen ? '关闭' : '打开' }}</button>
</div>
</template>组合式函数最佳实践:
- 命名以
use开头 - 返回值使用响应式引用
- 在
setup或<script setup>中调用 - 返回的函数保持响应式引用的连接
三、响应式系统重构(Proxy)
Vue 3 使用 Proxy 重写了响应式系统,解决了 Vue 2 的诸多限制。
1. Vue 2 响应式限制
javascript
// Vue 2 的限制
// 1. 无法检测对象属性的添加/删除
this.obj.newProp = 'value' // 不是响应式的
Vue.set(this.obj, 'newProp', 'value') // 需要使用 Vue.set
// 2. 无法检测数组索引赋值
this.arr[0] = 'new value' // 不是响应式的
Vue.set(this.arr, 0, 'new value') // 需要使用 Vue.set
// 3. 无法检测数组长度修改
this.arr.length = 0 // 不是响应式的
// 4. 对象新增属性需要 Vue.set2. Vue 3 Proxy 响应式
vue
<script setup>
import { reactive, ref } from 'vue'
// 对象
const obj = reactive({ name: '张三' })
obj.age = 25 // 自动响应式!
delete obj.name // 自动响应式!
// 数组
const arr = ref([1, 2, 3])
arr.value[0] = 100 // 自动响应式!
arr.value.length = 0 // 自动响应式!
// Map 和 Set
const map = reactive(new Map())
map.set('key', 'value') // 自动响应式!
const set = reactive(new Set())
set.add('item') // 自动响应式!
</script>3. 响应式工具函数
vue
<script setup>
import {
reactive,
ref,
readonly,
shallowRef,
shallowReactive,
toRaw,
markRaw,
isReactive,
isRef,
toRef,
toRefs
} from 'vue'
// readonly - 只读响应式
const original = reactive({ count: 0 })
const copy = readonly(original)
// copy.count++ // 警告:目标是只读的
// shallowRef - 浅层响应式 ref
const shallow = shallowRef({ nested: { value: 1 } })
shallow.value = { nested: { value: 2 } } // 触发更新
shallow.value.nested.value = 3 // 不触发更新
// shallowReactive - 浅层响应式 reactive
const shallowObj = shallowReactive({
count: 1,
nested: { value: 2 }
})
shallowObj.count++ // 触发更新
shallowObj.nested.value = 3 // 不触发更新
// toRaw - 获取原始对象
const raw = toRaw(original)
// markRaw - 标记对象永不转为响应式
const obj = markRaw({})
const reactiveObj = reactive(obj) // obj 不会被转为响应式
// 类型检查
console.log(isReactive(original)) // true
console.log(isRef(shallow)) // true
</script>使用场景对比:
| API | 使用场景 |
|---|---|
reactive | 复杂对象状态管理 |
ref | 基本类型、需要替换整个对象 |
shallowRef | 大型不可变数据结构 |
shallowReactive | 只需要顶层响应式 |
readonly | 防止数据被修改 |
markRaw | 第三方库实例、不需要响应式的对象 |
四、Teleport 组件
Teleport 允许将组件渲染到 DOM 树的其他位置。
1. 基本用法
vue
<template>
<div class="container">
<h1>主容器</h1>
<!-- 将模态框渲染到 body 下 -->
<Teleport to="body">
<div class="modal" v-if="showModal">
<div class="modal-content">
<h2>模态框标题</h2>
<p>模态框内容</p>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<style>
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
</style>2. 实际应用示例
vue
<!-- Modal.vue -->
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="modelValue" class="modal-overlay" @click.self="close">
<div class="modal-container">
<header class="modal-header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
<button class="close-btn" @click="close">×</button>
</header>
<main class="modal-body">
<slot></slot>
</main>
<footer class="modal-footer">
<slot name="footer">
<button @click="close">取消</button>
<button class="primary" @click="confirm">确认</button>
</slot>
</footer>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { watch } from 'vue'
const props = defineProps({
modelValue: Boolean,
title: String
})
const emit = defineEmits(['update:modelValue', 'confirm'])
function close() {
emit('update:modelValue', false)
}
function confirm() {
emit('confirm')
close()
}
// 防止背景滚动
watch(
() => props.modelValue,
val => {
document.body.style.overflow = val ? 'hidden' : ''
}
)
</script>
<!-- 使用 -->
<template>
<button @click="showModal = true">打开模态框</button>
<Modal v-model="showModal" title="确认操作" @confirm="handleConfirm">
<p>确定要执行此操作吗?</p>
</Modal>
</template>3. 多个 Teleport
vue
<template>
<!-- 多个 Teleport 会按顺序追加到目标位置 -->
<Teleport to="#notifications">
<div class="notification">通知 1</div>
</Teleport>
<Teleport to="#notifications">
<div class="notification">通知 2</div>
</Teleport>
<!-- 动态目标 -->
<Teleport :to="targetSelector">
<div>动态传送</div>
</Teleport>
<!-- 禁用 Teleport -->
<Teleport to="body" :disabled="isDisabled">
<div>条件传送</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const targetSelector = ref('#app')
const isDisabled = ref(false)
</script>使用场景:
- 模态框、对话框
- 通知提示
- 全局加载动画
- 下拉菜单(避免被父容器 overflow 裁剪)
五、Fragments(多根节点)
Vue 3 组件支持多个根节点。
1. Vue 2 的限制
vue
<!-- Vue 2 - 必须有单一根节点 -->
<template>
<div>
<!-- 必须包裹 -->
<h1>标题</h1>
<p>内容</p>
</div>
</template>2. Vue 3 多根节点
vue
<!-- Vue 3 - 支持多根节点 -->
<template>
<h1>标题</h1>
<p>内容</p>
<footer>页脚</footer>
</template>
<!-- 实际应用:列表项组件 -->
<template>
<td>{{ item.name }}</td>
<td>{{ item.price }}</td>
<td>{{ item.quantity }}</td>
</template>
<!-- 实际应用:文本组件 -->
<template>
<span class="prefix">{{ prefix }}</span>
<span class="content">{{ content }}</span>
<span class="suffix">{{ suffix }}</span>
</template>3. Attributes 继承
vue
<script setup>
defineOptions({
inheritAttrs: false // 禁用自动继承
})
</script>
<template>
<!-- 多根节点时,需要手动绑定 $attrs -->
<header v-bind="$attrs">
<h1>标题</h1>
</header>
<main>
<slot></slot>
</main>
</template>使用场景:
- 表格单元格组件
- 文本格式化组件
- 减少不必要的 DOM 嵌套
六、Suspense 组件
Suspense 用于协调异步依赖的加载状态。
1. 基本用法
vue
<template>
<Suspense>
<!-- 默认插槽:异步组件 -->
<template #default>
<AsyncComponent />
</template>
<!-- fallback 插槽:加载状态 -->
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(
() => import('./components/HeavyComponent.vue')
)
</script>2. 异步 setup
vue
<!-- AsyncUser.vue -->
<template>
<div>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
</template>
<script setup>
const props = defineProps(['id'])
// async setup 会让组件变成异步依赖
const user = await fetch(`/api/users/${props.id}`).then(r => r.json())
</script>
<!-- 父组件 -->
<template>
<Suspense>
<AsyncUser :id="userId" />
<template #fallback>
<div class="skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-text"></div>
</div>
</template>
</Suspense>
</template>3. 错误处理
vue
<template>
<Suspense @pending="onPending" @resolve="onResolve" @fallback="onFallback">
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
<script setup>
import { onErrorCaptured, ref } from 'vue'
const error = ref(null)
onErrorCaptured(e => {
error.value = e
return false // 阻止错误继续传播
})
function onPending() {
console.log('开始加载')
}
function onResolve() {
console.log('加载完成')
}
function onFallback() {
console.log('显示 fallback')
}
</script>4. 嵌套 Suspense
vue
<template>
<Suspense>
<template #default>
<Header />
<main>
<Suspense suspensible>
<!-- suspensible: 等待父级 -->
<AsyncContent />
<template #fallback>
<ContentSkeleton />
</template>
</Suspense>
</main>
<Footer />
</template>
<template #fallback>
<FullPageLoader />
</template>
</Suspense>
</template>使用场景:
- 异步组件加载
- 数据预取
- 代码分割
- 服务端渲染 (SSR)
七、更好的 TypeScript 支持
Vue 3 源码使用 TypeScript 重写,提供了完整的类型支持。
1. 类型定义
typescript
<script setup lang="ts">
import { ref, reactive, computed, type Ref, type PropType } from 'vue'
// ref 类型
const count = ref<number>(0)
const message = ref<string>('hello')
// reactive 类型
interface User {
id: number
name: string
email: string
}
const user = reactive<User>({
id: 1,
name: '张三',
email: 'zhangsan@example.com'
})
// computed 类型
const doubleCount = computed<number>(() => count.value * 2)
// Props 类型
interface Props {
title: string
count?: number
items: string[]
user: {
name: string
age: number
}
}
const props = defineProps<Props>()
// 带默认值的 Props
const props = withDefaults(defineProps<Props>(), {
title: '默认标题',
count: 0
})
// Emits 类型
interface Emits {
(e: 'update', value: number): void
(e: 'delete', id: number): void
(e: 'change', name: string, age: number): void
}
const emit = defineEmits<Emits>()
// 复杂 Props 类型
const props = defineProps({
items: {
type: Array as PropType<string[]>,
required: true
},
config: {
type: Object as PropType<{ theme: string; size: number }>,
default: () => ({ theme: 'light', size: 14 })
}
})
</script>2. 泛型组件
vue
<!-- GenericList.vue -->
<script setup lang="ts" generic="T">
import { type PropType } from 'vue'
interface Props {
items: T[]
renderItem: (item: T) => string
}
defineProps<Props>()
</script>
<template>
<ul>
<li v-for="(item, index) in items" :key="index">
{{ renderItem(item) }}
</li>
</ul>
</template>
<!-- 使用 -->
<script setup lang="ts">
import GenericList from './GenericList.vue'
interface User {
id: number
name: string
}
const users: User[] = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]
</script>
<template>
<GenericList :items="users" :render-item="user => user.name" />
</template>3. 类型工具
typescript
import type {
Ref,
ComputedRef,
ReactiveEffect,
ComponentPublicInstance,
VNode,
Component,
PropType,
ExtractPropTypes
} from 'vue'
// 提取 Props 类型
const propsDefinition = {
title: String,
count: { type: Number, default: 0 }
} as const
type Props = ExtractPropTypes<typeof propsDefinition>
// { title: string; count: number }
// 组件实例类型
type MyComponentInstance = ComponentPublicInstance<
{},
{},
{ localState: string },
{ method(): void }
>
// VNode 类型
const vnode: VNode = h('div', 'hello')八、v-model 改进
Vue 3 对 v-model 进行了重大改进。
1. Vue 2 vs Vue 3
vue
<!-- Vue 2 -->
<MyComponent v-model="value" />
<!-- 等价于 -->
<MyComponent :value="value" @input="value = $event" />
<!-- Vue 3 -->
<MyComponent v-model="value" />
<!-- 等价于 -->
<MyComponent :modelValue="value" @update:modelValue="value = $event" />2. 多个 v-model
vue
<!-- 父组件 -->
<template>
<UserForm
v-model:firstName="firstName"
v-model:lastName="lastName"
v-model:email="email"
/>
</template>
<!-- UserForm.vue -->
<script setup>
const firstName = defineModel < string > 'firstName'
const lastName = defineModel < string > 'lastName'
const email = defineModel < string > 'email'
</script>
<template>
<input v-model="firstName" placeholder="名" />
<input v-model="lastName" placeholder="姓" />
<input v-model="email" placeholder="邮箱" />
</template>3. v-model 修饰符
vue
<!-- 使用内置修饰符 -->
<MyInput v-model.trim="text" />
<MyInput v-model.number="count" />
<MyInput v-model.lazy="value" />
<!-- 自定义修饰符 -->
<script setup>
const [modelValue, modelModifiers] = defineModel({
set(value) {
if (modelModifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})
</script>
<template>
<input :value="modelValue" @input="modelValue = $event.target.value" />
</template>
<!-- 使用 -->
<MyInput v-model.capitalize="name" />4. 自定义 v-model 参数
vue
<script setup>
// 定义带参数的 v-model
const title =
defineModel <
string >
('title',
{
default: '默认标题',
required: true,
type: String,
validator: value => value.length > 0
})
</script>
<template>
<input v-model="title" />
</template>
<!-- 使用 -->
<CustomInput v-model:title="pageTitle" />九、指令 API 变化
1. 指令钩子变化
typescript
// Vue 2
const 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) {
// 指令与元素解绑时
}
}
// Vue 3
const directive = {
created(el, binding, vnode, prevVnode) {
// 元素被插入 DOM 前
},
beforeMount(el, binding, vnode, prevVnode) {
// 元素被插入 DOM 前(类似 bind)
},
mounted(el, binding, vnode, prevVnode) {
// 元素被插入 DOM 后(类似 inserted)
},
beforeUpdate(el, binding, vnode, prevVnode) {
// VNode 更新前
},
updated(el, binding, vnode, prevVnode) {
// VNode 更新后(类似 componentUpdated)
},
beforeUnmount(el, binding, vnode, prevVnode) {
// 卸载前
},
unmounted(el, binding, vnode, prevVnode) {
// 卸载后(类似 unbind)
}
}2. 自定义指令示例
typescript
// directives/vFocus.ts
import type { Directive, DirectiveBinding } from 'vue'
export const vFocus: Directive<HTMLElement> = {
mounted(el) {
el.focus()
}
}
// directives/vClickOutside.ts
import type { Directive } from 'vue'
export const vClickOutside: Directive<HTMLElement, (e: MouseEvent) => void> = {
mounted(el, binding) {
el._clickOutside = (event: MouseEvent) => {
if (!(el === event.target || el.contains(event.target as Node))) {
binding.value(event)
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutside)
}
}
// directives/vPermission.ts
import type { Directive } from 'vue'
import { useUserStore } from '@/stores/user'
export const vPermission: Directive<HTMLElement, string[]> = {
mounted(el, binding) {
const userStore = useUserStore()
const { value: permissions } = binding
const hasPermission = permissions.some(permission =>
userStore.permissions.includes(permission)
)
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
}
}
// directives/vLazy.ts - 图片懒加载
import type { Directive } from 'vue'
export const vLazy: Directive<HTMLImageElement, string> = {
mounted(el, binding) {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
})
observer.observe(el)
el._observer = observer
},
unmounted(el) {
el._observer?.disconnect()
}
}3. 全局注册指令
typescript
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { vFocus, vClickOutside, vPermission, vLazy } from './directives'
const app = createApp(App)
app.directive('focus', vFocus)
app.directive('click-outside', vClickOutside)
app.directive('permission', vPermission)
app.directive('lazy', vLazy)
app.mount('#app')十、全局 API 重构
Vue 3 重构了全局 API,支持 Tree-shaking。
1. createApp
typescript
// Vue 2
import Vue from 'vue'
import App from './App.vue'
import Router from './router'
import Store from './store'
Vue.use(Router)
Vue.use(Store)
Vue.prototype.$http = http
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
// Vue 3
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(router)
app.use(createPinia())
app.provide('http', http) // 全局注入
app.mount('#app')2. 全局配置
typescript
// Vue 2
Vue.config.ignoredElements = ['my-custom-element']
Vue.config.productionTip = false
// Vue 3
const app = createApp(App)
app.config.isCustomElement = tag => tag.startsWith('my-')
app.config.errorHandler = (err, instance, info) => {
console.error(err, info)
}
app.config.warnHandler = (msg, instance, trace) => {
console.warn(msg, trace)
}
app.config.performance = true3. 应用级 API
typescript
const app = createApp(App)
// 全局组件
app.component('MyButton', {
template: '<button><slot /></button>'
})
// 全局指令
app.directive('focus', {
mounted(el) {
el.focus()
}
})
// 全局 mixin(不推荐)
app.mixin({
created() {
console.log('全局 mixin')
}
})
// 全局 provide
app.provide('globalConfig', {
theme: 'dark',
locale: 'zh-CN'
})
// 多个应用实例
const app1 = createApp(App1).mount('#app1')
const app2 = createApp(App2).mount('#app2')十一、性能优化
1. 编译器优化
vue
<!-- 静态提升 -->
<template>
<div>
<!-- 静态内容会被提升到 render 函数外 -->
<div class="static">
<span>静态文本</span>
</div>
<!-- 动态内容 -->
<div>{{ dynamicContent }}</div>
</div>
</template>
<!-- 编译结果(简化) -->
<script>
const _hoisted_1 = /*#__PURE__*/ _createElementVNode(
'div',
{ class: 'static' },
[/*#__PURE__*/ _createElementVNode('span', null, '静态文本')],
-1
)
function render() {
return _createElementBlock('div', null, [
_hoisted_1, // 静态节点复用
_createElementVNode('div', null, _toDisplayString(dynamicContent))
])
}
</script>2. Patch Flags
vue
<template>
<!-- 不同的 patch flags -->
<div class="static">静态 class</div>
<div :class="dynamicClass">动态 class</div>
<div :style="dynamicStyle">动态 style</div>
<div>{{ text }}>文本内容</div>
<div :id="dynamicId" :class="dynamicClass">多个动态属性</div>
</template>
<!-- 编译后的 patch flags -->
<!--
1 = TEXT - 文本内容动态
2 = CLASS - class 动态
4 = STYLE - style 动态
8 = PROPS - 属性动态
16 = FULL_PROPS - 完整属性动态
32 = HYDRATE_EVENTS - 事件监听器
64 = STABLE_FRAGMENT - 稳定的片段
128 = KEYED_FRAGMENT - 带 key 的片段
256 = UNKEYED_FRAGMENT - 不带 key 的片段
-->3. Tree-shaking
typescript
// Vue 2 - 全量引入
import Vue from 'vue'
// 即使只用了 ref,也会打包整个 Vue
// Vue 3 - 按需引入
import { ref, computed, watch, onMounted } from 'vue'
// 只打包使用到的 API
// 未使用的 API 会被移除
import { ref } from 'vue'
// computed, watch 等未使用的不会被打包4. 虚拟列表优化
vue
<script setup>
import { ref } from 'vue'
const items = ref(
Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
}))
)
const visibleStart = ref(0)
const visibleEnd = ref(20)
const itemHeight = 50
function onScroll(e) {
const scrollTop = e.target.scrollTop
visibleStart.value = Math.floor(scrollTop / itemHeight)
visibleEnd.value = visibleStart.value + 20
}
const visibleItems = computed(() =>
items.value.slice(visibleStart.value, visibleEnd.value)
)
</script>
<template>
<div class="container" @scroll="onScroll">
<div class="spacer" :style="{ height: `${items.length * itemHeight}px` }">
<div
v-for="item in visibleItems"
:key="item.id"
class="item"
:style="{
height: `${itemHeight}px`,
transform: `translateY(${item.id * itemHeight}px)`
}"
>
{{ item.text }}
</div>
</div>
</div>
</template>十二、其他重要特性
1. defineComponent
typescript
import { defineComponent, ref, PropType } from 'vue'
export default defineComponent({
name: 'MyComponent',
props: {
title: {
type: String,
required: true
},
items: {
type: Array as PropType<string[]>,
default: () => []
}
},
setup(props, { emit, slots, attrs, expose }) {
const count = ref(0)
function increment() {
count.value++
emit('update', count.value)
}
expose({ increment }) // 暴露给父组件
return { count, increment }
}
})2. defineAsyncComponent
typescript
import { defineAsyncComponent } from 'vue'
// 简单用法
const AsyncComponent = defineAsyncComponent(
() => import('./components/MyComponent.vue')
)
// 完整选项
const AsyncComponentWithOptions = defineAsyncComponent({
loader: () => import('./components/MyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 10000,
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
retry()
} else {
fail()
}
}
})3. defineCustomElement
typescript
import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
props: {
message: String
},
template: `
<style>
:host { display: block; }
.text { color: blue; }
</style>
<span class="text">{{ message }}</span>
`
})
customElements.define('my-vue-element', MyVueElement)
// 使用
// <my-vue-element message="Hello"></my-vue-element>4. h 函数(渲染函数)
typescript
import { h, defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const count = ref(0)
return () => h('div', [
h('h1', '计数器'),
h('p', `当前值: ${count.value}`),
h('button', {
onClick: () => count.value++
}, '增加')
])
}
})
// JSX 写法(需要配置)
export default defineComponent({
setup() {
const count = ref(0)
return () => (
<div>
<h1>计数器</h1>
<p>当前值: {count.value}</p>
<button onClick={() => count.value++}>增加</button>
</div>
)
}
})5. CSS 中的 v-bind
vue
<template>
<div class="dynamic-color">动态颜色</div>
</template>
<script setup>
import { ref } from 'vue'
const themeColor = ref('#42b883')
const fontSize = ref(16)
</script>
<style>
.dynamic-color {
color: v-bind(themeColor);
font-size: v-bind(fontSize + 'px');
}
</style>6. CSS Scoped 改进
vue
<template>
<!-- 深度选择器 -->
<div class="container">
<ChildComponent />
</div>
</template>
<style scoped>
/* Vue 2 深度选择器 */
.container ::v-deep .child-class {
}
/* Vue 3 深度选择器(推荐) */
.container :deep(.child-class) {
}
/* 插槽选择器 */
:slotted(.slot-class) {
}
/* 全局选择器 */
:global(.global-class) {
}
</style>十三、迁移指南要点
1. 破坏性变更概览
| 变更项 | Vue 2 | Vue 3 |
|---|---|---|
| 全局 API | new Vue() | createApp() |
| v-model | value + input | modelValue + update:modelValue |
| v-if 与 v-for | v-for 优先级更高 | v-if 优先级更高 |
| 生命周期 | destroyed | unmounted |
| 过滤器 | 支持 | 移除 |
| $on/$off/$once | 支持 | 移除 |
| 内联模板 | 支持 | 移除 |
2. 迁移构建版本
javascript
// vue.config.js
module.exports = {
chainWebpack: config => {
config.resolve.alias.set('vue', '@vue/compat')
}
}3. 兼容性配置
javascript
import { createApp } from 'vue'
const app = createApp(App)
app.config.compilerOptions = {
compatConfig: {
MODE: 2, // 兼容模式
GLOBAL_MOUNT: false
}
}十四、最佳实践总结
1. 响应式选择
基本类型 → ref
对象类型 → reactive
需要替换整个对象 → ref
需要解构 → reactive + toRefs
大型不可变数据 → shallowRef
第三方类实例 → markRaw + shallowRef2. 组件设计原则
单一职责 → 拆分为小组件
逻辑复用 → 组合式函数
跨组件通信 → provide/inject 或状态管理
大型表单 → 组合式函数 + 分步组件3. 性能优化策略
大型列表 → 虚拟滚动
异步组件 → defineAsyncComponent + Suspense
静态内容 → v-once
频繁更新 → shallowRef/shallowReactive
计算缓存 → computed总结
Vue 3 带来了革命性的改进:
- Composition API 提供了更灵活的代码组织方式和更好的逻辑复用能力
- Proxy 响应式系统 解决了 Vue 2 的响应式限制
- 新内置组件(Teleport、Suspense、Fragment)扩展了组件能力
- TypeScript 支持 提供了完整的类型推断
- 性能优化 通过编译器优化和 Tree-shaking 减小了打包体积
- 更好的开发体验 更清晰的 API 设计和更好的错误提示
这些特性使得 Vue 3 成为构建现代 Web 应用的强大工具,无论是小型项目还是大型企业级应用都能从中受益。