11. 组合式 API
11.1 为什么需要组合式 API
javascript
// 选项式 API 的问题
export default {
data() { return { a: 1, b: 2 } },
computed: { sum() { return this.a + this.b } },
watch: { a() { /* 逻辑 */ } },
methods: { method1() { /* 逻辑 */ } },
// 相关逻辑分散在不同选项中
}
// 组合式 API 的优势:逻辑关注点聚焦
import { ref, computed, watch } from 'vue'
export default {
setup() {
const a = ref(1)
const b = ref(2)
const sum = computed(() => a.value + b.value)
watch(a, () => { /* 逻辑 */ })
const method1 = () => { /* 逻辑 */ }
// 所有相关逻辑在一起
return { a, b, sum, method1 }
}
}
11.2 setup() 函数
vue
<script>
export default {
// setup 函数
setup(props, context) {
// props: 响应式的 props 对象
// context: 上下文对象,包含:
// - attrs: 非响应式对象,接收未在 props 中声明的属性
// - slots: 插槽对象
// - emit: 触发事件的函数
// - expose: 暴露公共属性的函数
console.log(props.title)
console.log(context.attrs)
const count = ref(0)
// 暴露给模板
return {
count
}
// 或者使用渲染函数
// return () => h('div', count.value)
}
}
</script>
11.3<script setup>语法糖
vue
<script setup>
// 1. 顶层绑定自动暴露给模板
const count = ref(0)
const increment = () => count.value++
// 2. 自动导入组件
import MyComponent from './MyComponent.vue'
// 可以直接在模板中使用 <MyComponent />
// 3. 使用 defineProps 和 defineEmits
const props = defineProps(['title'])
const emit = defineEmits(['update'])
// 4. 使用 defineExpose 暴露组件实例
defineExpose({
count,
increment
})
// 5. 使用 useSlots 和 useAttrs
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
// 6. 顶层 await
const data = await fetchData()
// 7. 自定义指令
const vMyDirective = {
mounted(el) {
el.focus()
}
}
</script>
11.4 组合式函数 (Composables)
javascript
// useMouse.js - 鼠标位置跟踪
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
const update = (event) => {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// useFetch.js - 数据获取
import { ref, watch } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
try {
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
watch(() => url, fetchData, { immediate: true })
return { data, error, loading, retry: fetchData }
}
// useLocalStorage.js - 本地存储
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const data = ref(defaultValue)
// 从 localStorage 读取
const storedValue = localStorage.getItem(key)
if (storedValue !== null) {
data.value = JSON.parse(storedValue)
}
// 监听变化并保存到 localStorage
watch(data, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return data
}
// 在组件中使用
<script setup>
import { useMouse, useFetch, useLocalStorage } from './composables'
const { x, y } = useMouse()
const { data, loading, error } = useFetch('/api/data')
const settings = useLocalStorage('settings', { theme: 'dark' })
</script>
11.5 依赖注入模式
javascript
// useTheme.js - 主题管理
import { inject, provide, readonly, ref } from 'vue'
const ThemeSymbol = Symbol('theme')
export function provideTheme() {
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide(ThemeSymbol, {
theme: readonly(theme), // 只读,防止子组件直接修改
toggleTheme
})
}
export function useTheme() {
const themeContext = inject(ThemeSymbol)
if (!themeContext) {
throw new Error('必须在 ThemeProvider 中使用')
}
return themeContext
}
// 在根组件中
<script setup>
import { provideTheme } from './useTheme'
provideTheme()
</script>
// 在子组件中
<script setup>
import { useTheme } from './useTheme'
const { theme, toggleTheme } = useTheme()
</script>
12. 渲染函数和 JSX
12.1 渲染函数基础
vue
<script>
import { h } from 'vue'
export default {
render() {
// h() 创建虚拟 DOM
return h(
'div', // 标签名
{ id: 'app', class: 'container' }, // 属性
[
h('h1', 'Hello World'),
h('p', '这是内容'),
this.$slots.default?.() // 插槽内容
]
)
}
}
</script>
12.2 渲染函数参数
javascript
export default {
render() {
// h 函数的三种形式:
// 1. h('div') - 元素
// 2. h(MyComponent) - 组件
// 3. h('div', {}, children) - 带子节点
return h(
'button',
{
// 属性
class: ['btn', { 'btn-primary': this.primary }],
style: { color: this.color },
// 事件
onClick: this.handleClick,
// 其他属性
id: 'my-button',
'data-test': 'button'
},
[
// 子节点
h('span', '按钮'),
this.$slots.icon && h('span', this.$slots.icon())
]
)
}
}
12.3 使用 JSX
vue
<script>
// 配置 vite.config.js 支持 JSX
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
// JSX 语法
return () => (
<div class="container">
<h1>计数器: {count.value}</h1>
<button onClick={increment}>增加</button>
<ChildComponent title="子组件" />
</div>
)
}
})
// 或者使用 .jsx/.tsx 文件
import { defineComponent } from 'vue'
const MyComponent = defineComponent({
render() {
return (
<div>
<slot name="header" />
<main>{this.$slots.default?.()}</main>
<slot name="footer" />
</div>
)
}
})
</script>
12.4 函数式组件 + JSX
jsx
// FunctionalComponent.jsx
import { defineComponent } from 'vue'
const FunctionalComponent = (props, { slots, emit, attrs }) => {
const handleClick = () => {
emit('click', props.id)
}
return (
<div {...attrs} onClick={handleClick}>
{slots.default?.()}
{props.title && <h2>{props.title}</h2>}
</div>
)
}
FunctionalComponent.props = ['id', 'title']
FunctionalComponent.emits = ['click']
export default FunctionalComponent
13. TypeScript 支持
13.1 组件类型定义
vue
<!-- 选项式 API + TypeScript -->
<script lang="ts">
import { defineComponent } from 'vue'
interface User {
id: number
name: string
email: string
}
export default defineComponent({
name: 'UserProfile',
props: {
// Prop 类型
userId: {
type: Number as () => number,
required: true
},
user: {
type: Object as () => User,
default: () => ({ id: 0, name: '', email: '' })
},
tags: {
type: Array as () => string[],
default: () => []
}
},
data() {
return {
loading: false as boolean,
error: null as string | null
}
},
computed: {
// 计算属性类型
fullName(): string {
return this.user.name.toUpperCase()
}
},
methods: {
// 方法类型
updateUser(user: User): Promise<void> {
return api.updateUser(user)
}
}
})
</script>
13.2 组合式 API + TypeScript
vue
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
// 接口定义
interface Props {
title: string
count?: number
items: string[]
user?: {
name: string
age: number
}
}
interface Emits {
(e: 'update:title', value: string): void
(e: 'submit', data: { id: number; name: string }): void
}
// 类型化的 props
const props = defineProps<Props>()
// 类型化的 emits
const emit = defineEmits<Emits>()
// 响应式数据
const count = ref<number>(0) // 明确类型
const user = ref<User | null>(null) // 可为 null
// 计算属性
const total = computed<number>(() => {
return props.count || 0 + count.value
})
// 异步函数
const fetchData = async (): Promise<void> => {
const response = await fetch('/api/data')
const data = await response.json()
user.value = data
}
</script>
13.3 泛型组件
vue
<!-- GenericComponent.vue -->
<script setup lang="ts" generic="T extends { id: number; name: string }">
import { ref } from 'vue'
const props = defineProps<{
items: T[]
selected: T | null
}>()
const emit = defineEmits<{
select: [item: T]
}>()
const selectItem = (item: T) => {
emit('select', item)
}
</script>
<!-- 使用 -->
<template>
<GenericComponent
:items="users"
:selected="selectedUser"
@select="handleSelect"
/>
</template>
13.4 全局类型扩展
typescript
// src/types/vue.d.ts
import { ComponentCustomProperties } from 'vue'
declare module 'vue' {
interface ComponentCustomProperties {
$filters: {
formatDate: (date: Date) => string
formatCurrency: (amount: number) => string
}
$api: {
fetchUser: (id: number) => Promise<User>
}
}
// 全局组件类型
interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}
14. 性能优化
14.1 响应式优化
javascript
// 1. 使用 shallowRef 和 shallowReactive
import { shallowRef, shallowReactive } from 'vue'
// 只追踪 .value 的变化
const largeObject = shallowRef({ /* 大对象 */ })
// 只追踪顶层属性的变化
const shallowState = shallowReactive({
nested: { a: 1 } // nested 的变化不会触发响应
})
// 2. 使用 markRaw 跳过响应式转换
import { markRaw } from 'vue'
const nonReactiveConfig = markRaw({
// 不会被转换为响应式
constantValue: '不会变化'
})
// 3. 使用 computed 缓存计算结果
const expensiveValue = computed(() => {
// 复杂计算
return heavyCalculation()
})
// 4. 避免不必要的响应式
const nonReactiveArray = Object.freeze([1, 2, 3]) // 冻结数组
const nonReactiveObject = Object.freeze({ a: 1 }) // 冻结对象
14.2 组件优化
vue
<!-- 1. 使用 v-once -->
<template>
<div v-once>
<h1>{{ title }}</h1> <!-- 只渲染一次 -->
</div>
</template>
<!-- 2. 使用 v-memo -->
<template>
<div v-memo="[valueA, valueB]">
<!-- 只有 valueA 或 valueB 变化时才更新 -->
{{ valueA }} {{ valueB }}
</div>
</template>
<!-- 3. 懒加载组件 -->
<script setup>
import { defineAsyncComponent } from 'vue'
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
</script>
<!-- 4. 函数式组件(轻量级) -->
<script>
export default {
functional: true,
props: ['item'],
render(h, { props }) {
return h('div', props.item.name)
}
}
</script>
14.3 列表优化
vue
<template>
<!-- 1. 使用 key -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<!-- 2. 虚拟滚动 -->
<!-- 使用 vue-virtual-scroller 等库 -->
<!-- 3. 分页/懒加载 -->
<div v-for="item in visibleItems">
{{ item.name }}
</div>
<button @click="loadMore">加载更多</button>
</template>
<script setup>
import { computed } from 'vue'
const items = ref([])
const page = ref(1)
const pageSize = 20
// 只渲染当前页的数据
const visibleItems = computed(() => {
const start = (page.value - 1) * pageSize
return items.value.slice(start, start + pageSize)
})
</script>
14.4 内存优化
javascript
// 1. 及时清理副作用
import { onUnmounted } from 'vue'
onMounted(() => {
const timer = setInterval(() => {
// 定时任务
}, 1000)
// 组件销毁时清理
onUnmounted(() => clearInterval(timer))
})
// 2. 避免内存泄漏
const listeners = []
onMounted(() => {
listeners.push(
window.addEventListener('resize', handleResize)
)
})
onUnmounted(() => {
listeners.forEach(removeListener => removeListener())
})
// 3. 使用 WeakMap/WeakSet 存储临时数据
const cache = new WeakMap()
function getExpensiveValue(obj) {
if (!cache.has(obj)) {
const value = computeExpensiveValue(obj)
cache.set(obj, value)
}
return cache.get(obj)
}
15. 最佳实践
15.1 项目结构
text
src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── common/ # 通用组件
│ ├── layout/ # 布局组件
│ └── ui/ # UI 组件
├── composables/ # 组合式函数
├── views/ # 页面组件
├── router/ # 路由配置
├── store/ # 状态管理
├── api/ # API 接口
├── utils/ # 工具函数
├── types/ # TypeScript 类型
└── styles/ # 样式文件
15.2 代码规范
javascript
// 1. 命名规范
const userData = ref(null) // ✅ 描述性名称
const data = ref(null) // ❌ 过于模糊
const getUserInformation = () => {} // ✅ 动词开头
const userInfo = () => {} // ❌ 名词开头
// 2. 组件命名
MyComponent.vue // ✅ PascalCase
my-component.vue // ✅ kebab-case(模板中使用)
MyComponent.ts // ✅ TypeScript 组件
// 3. 文件组织
// 单文件组件顺序
<template>
<!-- 1. 模板 -->
</template>
<script setup>
// 2. 脚本
</script>
<style scoped>
/* 3. 样式 */
</style>
// 4. 导入顺序
// 1) Vue 核心 API
// 2) 第三方库
// 3) 本地模块
// 4) 类型导入
15.3 错误处理
javascript
// 1. 全局错误处理
import { createApp } from 'vue'
const app = createApp(App)
app.config.errorHandler = (err, instance, info) => {
console.error('Vue 错误:', err)
// 发送错误日志
}
// 2. Promise 错误处理
const fetchData = async () => {
try {
const data = await api.getData()
return data
} catch (error) {
console.error('请求失败:', error)
// 显示用户友善的错误信息
showError('加载失败,请重试')
throw error // 重新抛出供上层处理
}
}
// 3. 错误边界组件
<template>
<slot v-if="hasError" name="fallback" />
<slot v-else />
</template>
<script setup>
import { ref, onErrorCaptured } from 'vue'
const hasError = ref(false)
onErrorCaptured((error, instance, info) => {
hasError.value = true
console.error('子组件错误:', error)
// 返回 false 阻止错误继续向上传播
return false
})
</script>
15.4 测试
javascript
// 1. 单元测试示例(使用 Vitest)
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter', () => {
it('点击按钮增加计数', async () => {
const wrapper = mount(Counter)
// 初始状态
expect(wrapper.find('.count').text()).toBe('0')
// 触发事件
await wrapper.find('button').trigger('click')
// 断言结果
expect(wrapper.find('.count').text()).toBe('1')
})
})
// 2. 组合式函数测试
import { useCounter } from './useCounter'
import { renderHook, act } from '@testing-library/vue'
describe('useCounter', () => {
it('应该增加计数', () => {
const { result } = renderHook(() => useCounter())
expect(result.current.count.value).toBe(0)
act(() => {
result.current.increment()
})
expect(result.current.count.value).toBe(1)
})
})
这是 Vue 3 核心知识的第三部分,涵盖了组合式 API、渲染函数、TypeScript 支持、性能优化和最佳实践。
© 版权声明
文章版权归作者所有,未经允许请勿转载。如内容涉嫌侵权,请在本页底部进入<联系我们>进行举报投诉!
THE END



















暂无评论内容