本篇定位 :组合式 API 是 Vue 3 最核心、最具突破性的特性,它从根本上改变了 Vue 组件逻辑的编写方式。本篇将深入讲解组合式 API 的入口------
setup函数,以及与之配套的核心响应式 API。你将理解组合式 API 的设计哲学,掌握<script setup>语法糖的完整用法,并能够熟练运用响应式 API 构建复杂的应用逻辑。
一、为什么需要组合式 API?
1.1 选项式 API 的局限性
在 Vue 2 中,我们使用选项式 API (Options API)编写组件,通过 data、methods、computed、watch、mounted 等选项来组织代码:
javascript
export default {
data() {
return {
count: 0,
user: null,
isLoading: false
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
},
fetchUser() { /* ... */ }
},
watch: {
count(newVal) {
console.log('count changed', newVal)
}
},
mounted() {
this.fetchUser()
}
}
当组件变得复杂时,选项式 API 会出现一个核心问题:关注点分散。
一个功能的逻辑(数据、计算属性、方法、生命周期钩子)被强制拆分到不同的选项中。当组件有多个功能时,代码会变得难以阅读和维护:
data: [功能A相关数据] [功能B相关数据] [功能C相关数据]
methods: [功能A方法] [功能B方法] [功能C方法]
computed:[功能A计算] [功能B计算] [功能C计算]
watch: [功能A侦听] [功能B侦听] [功能C侦听]
mounted: [功能A初始化] [功能B初始化] [功能C初始化]
1.2 组合式 API 的解决方案
组合式 API 允许我们将属于同一个功能的代码聚合在一起,而不是分散在不同选项中:
vue
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// -------- 功能 A:计数器 --------
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
watch(count, (newVal) => {
console.log('count changed', newVal)
})
// -------- 功能 B:用户数据 --------
const user = ref(null)
const isLoading = ref(false)
async function fetchUser() {
isLoading.value = true
user.value = await api.getUser()
isLoading.value = false
}
onMounted(() => {
fetchUser()
})
</script>
组合式 API 的核心优势:
- 逻辑复用 :可以将功能封装为组合式函数(Composables),在多个组件间共享
- 更好的代码组织:按功能聚合,而非按选项类型
- 更好的 TypeScript 支持:类型推导更自然
- 更小的生产包体积:更好的 Tree Shaking 支持
二、setup 函数:组合式 API 的入口
2.1 基本用法
setup 是 Vue 3 中组件选项 的一部分,它在组件实例创建之前执行,是组合式 API 的入口点。
vue
<script>
import { ref } from 'vue'
export default {
setup(props, context) {
// 在这里使用组合式 API
const count = ref(0)
function increment() {
count.value++
}
// 返回的数据和方法将暴露给模板
return {
count,
increment
}
}
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
2.2 setup 的执行时机
setup 在 beforeCreate 之前执行,此时组件实例尚未创建。这意味着:
setup中不能访问this(this为undefined)setup中不能访问组件实例上的属性 (如this.$router、this.$emit等)setup中不能使用选项式 API (如data、methods等)
生命周期执行顺序:
setup() → beforeCreate → created → onBeforeMount → onMounted → ...
2.3 setup 的语法糖:<script setup>
<script setup> 是 Vue 3.2 引入的编译时语法糖,它让组合式 API 的写法更加简洁:
vue
<script setup>
// 所有顶层绑定自动暴露给模板
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
<script setup> 做了哪些事?
- 自动将顶层声明的变量、函数、导入的组件暴露给模板
- 自动执行
setup逻辑,无需显式返回 - 更高效:编译时优化,减少运行时代码
- 更好的 TypeScript 支持
本系列教程将统一使用 <script setup> 语法。
2.4 setup 的参数(非 <script setup> 场景)
在不使用 <script setup> 时,setup 函数接收两个参数:
javascript
export default {
setup(props, context) {
// props:响应式的 props 对象
// context:包含 attrs、slots、emit 的对象
}
}
props:
- 是响应式的,当父组件传入新的 props 时,props 会更新
- 不能解构 props(会失去响应式),应使用
toRefs或toRef
context:
javascript
export default {
setup(props, { attrs, slots, emit, expose }) {
// attrs: 非 props 的属性(如 class、style、自定义属性)
// slots: 插槽对象
// emit: 触发自定义事件
// expose: 暴露公共 API(用于父组件通过 ref 访问)
}
}
在 <script setup> 中,这些功能通过其他方式实现:
defineProps接收 propsdefineEmits定义事件useSlots、useAttrs获取插槽和属性defineExpose暴露公共 API
三、核心响应式 API 深度解析
3.1 ref:基础响应式引用
ref 是最基础的响应式 API,它返回一个可变的响应式引用。
vue
<script setup>
import { ref } from 'vue'
// 基本类型
const count = ref(0)
const message = ref('Hello')
const isActive = ref(true)
// 对象类型(ref 也能处理对象)
const user = ref({ name: '张三', age: 25 })
// 访问:在 JavaScript 中需要通过 .value
console.log(count.value) // 0
count.value = 10
// 在模板中自动解包,不需要 .value
</script>
<template>
<p>{{ count }}</p>
<p>{{ message }}</p>
<p>{{ user.name }}</p>
</template>
ref 的深层响应式 :ref 对对象和数组会进行深层响应式处理,嵌套属性的变化也会触发更新。
javascript
const user = ref({ profile: { age: 25 } })
// 深层更新也能触发响应
user.value.profile.age = 30 // ✅ 触发更新
ref 的 unwrapping 规则:
- 在模板中作为顶层属性时自动解包
- 在
reactive对象中作为属性时自动解包 - 在数组或集合中不会自动解包
3.2 reactive:对象的响应式代理
reactive 返回一个对象的响应式代理,适用于对象、数组、Map、Set 等。
vue
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: '张三',
age: 25,
hobbies: ['阅读', '编程']
})
// 直接访问/修改,无需 .value
user.age = 30
user.hobbies.push('游泳')
</script>
<template>
<p>{{ user.name }},{{ user.age }}岁</p>
<p>爱好:{{ user.hobbies.join('、') }}</p>
</template>
reactive 的局限性:
- 只能用于对象类型,不能用于原始值
- 不能整体替换 :
user = { name: '李四' }会丢失响应式 - 解构会丢失响应式 :
const { name, age } = user后name和age是普通值 - 传递给函数时,如果函数对参数解构,也会丢失响应式
何时使用 reactive 而不是 ref?
- 当对象结构固定、不需要整体替换时
- 当你希望避免
.value的冗余时 - 当对象属性频繁读写时
3.3 ref 与 reactive 的对比
| 特性 | ref |
reactive |
|---|---|---|
| 支持的数据类型 | 任意类型 | 仅对象类型(对象、数组、集合) |
| 访问方式 | 需要 .value |
直接访问 |
| 整体替换 | ✅ 可以替换 .value |
❌ 不能直接替换 |
| 解构后保持响应式 | ✅ 通过 .value 仍可 |
❌ 解构后丢失 |
| 模板自动解包 | ✅ 顶层自动解包 | ✅ 直接使用 |
| 内部实现 | 基于 .value 的 getter/setter |
基于 Proxy |
| 适用场景 | 通用首选 | 固定结构的对象 |
官方推荐 :如果你不确定用哪个,优先使用 ref,因为它更通用、更灵活。
3.4 computed:计算属性
computed 用于创建派生状态 ,它会基于其他响应式数据计算新值,并缓存结果,只有依赖变化时才重新计算。
vue
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const price = ref(100)
const quantity = ref(3)
// 只读计算属性
const fullName = computed(() => {
return firstName.value + lastName.value
})
// 带格式化
const totalPrice = computed(() => {
return (price.value * quantity.value).toFixed(2)
})
// 可写计算属性(需要 getter 和 setter)
const fullNameEditable = computed({
get: () => firstName.value + ' ' + lastName.value,
set: (newValue) => {
const [first, last] = newValue.split(' ')
firstName.value = first
lastName.value = last
}
})
// 模板中使用
</script>
<template>
<p>全名:{{ fullName }}</p>
<p>总价:¥{{ totalPrice }}</p>
<input v-model="fullNameEditable" placeholder="输入姓名" />
</template>
computed vs method:
computed有缓存,依赖不变不重新计算method在每次渲染时都会执行
vue
<script setup>
const count = ref(0)
// 每次渲染都会重新计算
function getDouble() {
console.log('method 执行')
return count.value * 2
}
// 依赖不变时不会重新计算
const doubleComputed = computed(() => {
console.log('computed 执行')
return count.value * 2
})
</script>
<template>
<!-- count 不变,多次访问 doubleComputed 不会重新计算 -->
<p>{{ doubleComputed }}</p>
<p>{{ doubleComputed }}</p>
<!-- getDouble 每次都会执行 -->
<p>{{ getDouble() }}</p>
<p>{{ getDouble() }}</p>
</template>
3.5 watch:侦听数据变化
watch 用于侦听响应式数据的变化,并在变化时执行回调。
vue
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const user = ref({ name: '张三', age: 25 })
// 侦听单个 ref
watch(count, (newVal, oldVal) => {
console.log('count 从', oldVal, '变为', newVal)
})
// 侦听多个数据
watch([count, user], ([newCount, newUser], [oldCount, oldUser]) => {
console.log('count 或 user 变化')
})
// 侦听 ref 的深层变化
watch(user, (newVal, oldVal) => {
console.log('user 变化', newVal)
}, { deep: true })
// 立即执行(进入页面时先执行一次)
watch(count, (newVal) => {
console.log('立即执行', newVal)
}, { immediate: true })
// 侦听对象中的某个属性(使用 getter 函数)
watch(() => user.value.age, (newAge) => {
console.log('年龄变化为', newAge)
})
</script>
watch 的配置选项:
| 选项 | 说明 |
|---|---|
deep |
深层侦听(默认 false) |
immediate |
立即执行回调(默认 false) |
flush |
回调执行时机:'pre'(默认,更新前)、'post'(更新后)、'sync'(同步) |
watch vs watchEffect (watchEffect 将在下篇详述):
watch需要明确指定要侦听的数据watchEffect自动收集依赖,但无法访问旧值
3.6 toRef 与 toRefs:保持响应式
toRef :为 reactive 对象的某个属性创建一个 ref,并保持同步。
javascript
import { reactive, toRef } from 'vue'
const user = reactive({ name: '张三', age: 25 })
const nameRef = toRef(user, 'name')
// nameRef 与 user.name 同步
nameRef.value = '李四'
console.log(user.name) // '李四'
user.age = 30
console.log(nameRef.value) // '李四'(不变)
toRefs :将 reactive 对象的所有属性转换为 ref。
javascript
import { reactive, toRefs } from 'vue'
const user = reactive({ name: '张三', age: 25 })
const { name, age } = toRefs(user)
// 解构后仍然保持响应式
name.value = '李四' // 同步到 user
user.age = 30 // age.value 也会更新
主要用途 :在组合式函数中,将 reactive 对象解构为 ref,以便保持响应式。
3.7 readonly:只读代理
readonly 创建一个只读的响应式代理,任何修改操作都会被阻止。
javascript
import { reactive, readonly } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
// 修改 copy 会触发警告
copy.count++ // ❌ 警告:Set operation on key "count" failed
// 修改 original 会触发更新,copy 也会变化
original.count++ // ✅ 更新
使用场景 :在组合式函数中,暴露数据时使用 readonly 防止外部意外修改内部状态。
3.8 shallowRef 与 shallowReactive:浅层响应式
默认的 ref 和 reactive 都是深层响应式,会对所有嵌套属性进行代理。如果数据结构很深,且只关心根级别的变化,可以使用浅层版本来优化性能。
javascript
import { shallowRef, shallowReactive } from 'vue'
// 浅层 ref:只有 .value 的变化触发更新
const user = shallowRef({ name: '张三', profile: { age: 25 } })
user.value.name = '李四' // ❌ 不会触发更新
user.value = { name: '王五' } // ✅ 触发更新
// 浅层 reactive:只有根级属性变化触发更新
const settings = shallowReactive({
theme: 'dark',
preferences: { language: 'zh' }
})
settings.theme = 'light' // ✅ 触发更新
settings.preferences.language = 'en' // ❌ 不触发更新
3.9 triggerRef:强制更新
在某些特殊场景下(如使用第三方库修改了数据),你可能需要强制触发更新。
javascript
import { shallowRef, triggerRef } from 'vue'
const user = shallowRef({ name: '张三' })
// 直接修改对象内部属性,不会触发更新
user.value.name = '李四'
// 强制触发更新
triggerRef(user)
四、响应式 API 的最佳实践
4.1 何时使用 ref vs reactive?
决策流程图:
需要声明响应式数据
↓
数据类型是原始值(string/number/boolean...)?
↓ 是 ↓ 否(对象/数组)
必须使用 ref 考虑:
↓ - 是否经常整体替换?
需要解构吗? - 是否希望避免 .value?
↓ 是 ↓
使用 ref + toRefs 如果"是",用 reactive;否则用 ref
经验法则:
- 默认使用
ref,它更通用,限制更少 - 当对象属性需要频繁读写且不需要整体替换时,使用
reactive - 在同一组件中保持一致性,不要混用得过于随意
4.2 保持响应式数据的不可变性
在组合式函数中,建议使用 readonly 保护数据:
typescript
// composables/useUser.ts
import { ref, readonly } from 'vue'
export function useUser() {
const user = ref({ name: '张三', age: 25 })
function updateUser(newName: string) {
user.value.name = newName
}
// 暴露只读数据,同时提供修改方法
return {
user: readonly(user),
updateUser
}
}
4.3 避免过度使用 reactive
reactive 的解构限制是一个常见的陷阱:
vue
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
})
// ❌ 错误:解构后失去响应式
const { count, message } = state
function increment() {
count++ // 不会触发更新
}
// ✅ 正确:使用 toRefs
const { count, message } = toRefs(state)
function increment() {
count.value++ // ✅ 触发更新
}
</script>
4.4 类型推导与类型安全
使用 TypeScript 时,响应式 API 能自动推导类型:
typescript
import { ref, reactive, computed } from 'vue'
// 自动推导类型
const count = ref(0) // Ref<number>
const user = reactive({ name: '张三', age: 25 }) // { name: string; age: number }
// 显式类型声明
const list = ref<string[]>([]) // Ref<string[]>
const config = reactive<{ theme: 'dark' | 'light' }>({ theme: 'dark' })
五、综合实战:用户管理与搜索筛选
让我们综合运用本章所学 API,构建一个完整的用户管理页面。
vue
<script setup lang="ts">
import { ref, reactive, computed, watch, toRefs, readonly } from 'vue'
// -------- 数据类型定义 --------
interface User {
id: number
name: string
age: number
email: string
isActive: boolean
createdAt: string
}
// -------- 状态管理 --------
// 使用 ref 管理列表
const users = ref<User[]>([
{ id: 1, name: '张三', age: 25, email: 'zhangsan@example.com', isActive: true, createdAt: '2024-01-15' },
{ id: 2, name: '李四', age: 30, email: 'lisi@example.com', isActive: false, createdAt: '2024-02-20' },
{ id: 3, name: '王五', age: 28, email: 'wangwu@example.com', isActive: true, createdAt: '2024-03-10' },
{ id: 4, name: '赵六', age: 22, email: 'zhaoliu@example.com', isActive: true, createdAt: '2024-04-05' },
{ id: 5, name: '孙七', age: 35, email: 'sunqi@example.com', isActive: false, createdAt: '2024-05-12' }
])
// 使用 reactive 管理筛选和排序状态(固定结构,不需要整体替换)
const filterState = reactive({
keyword: '',
activeFilter: 'all' as 'all' | 'active' | 'inactive',
sortBy: 'name' as 'name' | 'age' | 'createdAt',
sortOrder: 'asc' as 'asc' | 'desc'
})
// 使用 ref 管理 UI 状态
const isLoading = ref(false)
const selectedUserId = ref<number | null>(null)
// -------- 计算属性:过滤和排序后的用户列表 --------
const filteredAndSortedUsers = computed(() => {
let result = [...users.value]
// 关键字搜索
if (filterState.keyword.trim()) {
const keyword = filterState.keyword.toLowerCase().trim()
result = result.filter(user =>
user.name.includes(keyword) ||
user.email.includes(keyword)
)
}
// 活跃状态过滤
if (filterState.activeFilter === 'active') {
result = result.filter(user => user.isActive)
} else if (filterState.activeFilter === 'inactive') {
result = result.filter(user => !user.isActive)
}
// 排序
result.sort((a, b) => {
let aVal: any = a[filterState.sortBy]
let bVal: any = b[filterState.sortBy]
if (typeof aVal === 'string') {
return filterState.sortOrder === 'asc'
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal)
} else {
return filterState.sortOrder === 'asc'
? aVal - bVal
: bVal - aVal
}
})
return result
})
// 统计信息
const stats = computed(() => {
const total = users.value.length
const active = users.value.filter(u => u.isActive).length
const inactive = total - active
return { total, active, inactive }
})
// -------- 侦听器:当筛选条件变化时记录日志(演示 watch) --------
watch(
() => ({ keyword: filterState.keyword, activeFilter: filterState.activeFilter }),
(newVal, oldVal) => {
console.log('筛选条件变化:', newVal)
},
{ deep: true }
)
// 侦听用户列表变化(深层)
watch(
users,
(newUsers) => {
console.log('用户列表更新,当前共', newUsers.length, '个用户')
},
{ deep: true }
)
// -------- 操作方法 --------
function addUser() {
const newUser: User = {
id: Date.now(),
name: `新用户${users.value.length + 1}`,
age: Math.floor(Math.random() * 30) + 20,
email: `user${Date.now()}@example.com`,
isActive: true,
createdAt: new Date().toISOString().slice(0, 10)
}
users.value.push(newUser)
}
function removeUser(id: number) {
users.value = users.value.filter(u => u.id !== id)
}
function toggleUserStatus(id: number) {
const user = users.value.find(u => u.id === id)
if (user) {
user.isActive = !user.isActive
}
}
function selectUser(id: number) {
selectedUserId.value = selectedUserId.value === id ? null : id
}
// 排序切换
function toggleSortBy(key: 'name' | 'age' | 'createdAt') {
if (filterState.sortBy === key) {
filterState.sortOrder = filterState.sortOrder === 'asc' ? 'desc' : 'asc'
} else {
filterState.sortBy = key
filterState.sortOrder = 'asc'
}
}
// 重置筛选
function resetFilters() {
filterState.keyword = ''
filterState.activeFilter = 'all'
filterState.sortBy = 'name'
filterState.sortOrder = 'asc'
}
// 模拟加载数据
async function fetchUsers() {
isLoading.value = true
// 模拟 API 请求
await new Promise(resolve => setTimeout(resolve, 1000))
// 这里可以添加新数据
isLoading.value = false
}
</script>
<template>
<div class="user-management">
<h1>👥 用户管理</h1>
<!-- 统计信息 -->
<div class="stats">
<span>总计:{{ stats.total }}</span>
<span class="active">活跃:{{ stats.active }}</span>
<span class="inactive">非活跃:{{ stats.inactive }}</span>
<button @click="fetchUsers" :disabled="isLoading">刷新数据</button>
<button @click="addUser">➕ 添加用户</button>
</div>
<!-- 筛选区 -->
<div class="filters">
<input
v-model="filterState.keyword"
placeholder="🔍 搜索姓名或邮箱..."
class="search-input"
/>
<select v-model="filterState.activeFilter" class="filter-select">
<option value="all">全部状态</option>
<option value="active">活跃</option>
<option value="inactive">非活跃</option>
</select>
<button @click="resetFilters" class="btn-reset">重置</button>
</div>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading">加载中...</div>
<!-- 用户列表 -->
<div v-else class="user-list">
<table class="user-table">
<thead>
<tr>
<th @click="toggleSortBy('name')" style="cursor: pointer;">
姓名 {{ filterState.sortBy === 'name' ? (filterState.sortOrder === 'asc' ? '▲' : '▼') : '⇅' }}
</th>
<th @click="toggleSortBy('age')" style="cursor: pointer;">
年龄 {{ filterState.sortBy === 'age' ? (filterState.sortOrder === 'asc' ? '▲' : '▼') : '⇅' }}
</th>
<th>邮箱</th>
<th @click="toggleSortBy('createdAt')" style="cursor: pointer;">
注册日期 {{ filterState.sortBy === 'createdAt' ? (filterState.sortOrder === 'asc' ? '▲' : '▼') : '⇅' }}
</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="user in filteredAndSortedUsers"
:key="user.id"
class="user-row"
:class="{ selected: selectedUserId === user.id }"
>
<td>{{ user.name }}</td>
<td>{{ user.age }}</td>
<td>{{ user.email }}</td>
<td>{{ user.createdAt }}</td>
<td>
<span :class="['status-badge', user.isActive ? 'active' : 'inactive']">
{{ user.isActive ? '✅ 活跃' : '❌ 非活跃' }}
</span>
</td>
<td>
<button @click="toggleUserStatus(user.id)" class="btn-toggle">
{{ user.isActive ? '禁用' : '启用' }}
</button>
<button @click="selectUser(user.id)" class="btn-detail">
{{ selectedUserId === user.id ? '收起' : '详情' }}
</button>
<button @click="removeUser(user.id)" class="btn-delete">删除</button>
</td>
</tr>
<!-- 详情行 -->
<tr v-if="selectedUserId !== null">
<td colspan="6" class="detail-row">
<div v-if="selectedUserId !== null">
<strong>用户详情:</strong>
<pre>{{ users.find(u => u.id === selectedUserId) }}</pre>
</div>
</td>
</tr>
</tbody>
</table>
<p v-if="filteredAndSortedUsers.length === 0" class="empty">
没有匹配的用户
</p>
</div>
</div>
</template>
<style scoped>
.user-management {
max-width: 1100px;
margin: 20px auto;
padding: 20px;
background: #f8fafc;
border-radius: 12px;
}
h1 { margin-bottom: 16px; }
.stats {
display: flex;
gap: 20px;
margin-bottom: 16px;
align-items: center;
flex-wrap: wrap;
}
.stats .active { color: #48bb78; font-weight: 600; }
.stats .inactive { color: #fc8181; font-weight: 600; }
.stats button {
padding: 6px 16px;
border: none;
border-radius: 6px;
background: #4299e1;
color: white;
cursor: pointer;
}
.stats button:disabled { opacity: 0.6; cursor: not-allowed; }
.filters {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.search-input {
flex: 1;
min-width: 200px;
padding: 8px 14px;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 14px;
}
.filter-select {
padding: 8px 14px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: white;
}
.btn-reset {
padding: 8px 16px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: white;
cursor: pointer;
}
.loading { text-align: center; padding: 40px; color: #a0aec0; }
.user-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.user-table th {
background: #f7fafc;
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: #4a5568;
border-bottom: 2px solid #e2e8f0;
}
.user-table td {
padding: 10px 16px;
border-bottom: 1px solid #edf2f7;
}
.user-row:hover { background: #f7fafc; }
.user-row.selected { background: #ebf8ff; }
.status-badge {
padding: 2px 12px;
border-radius: 12px;
font-size: 13px;
}
.status-badge.active { background: #f0fff4; color: #276749; }
.status-badge.inactive { background: #fff5f5; color: #9b2c2c; }
.btn-toggle, .btn-detail, .btn-delete {
padding: 4px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
margin-right: 4px;
}
.btn-toggle { background: #ebf8ff; color: #2b6cb0; }
.btn-toggle:hover { background: #bee3f8; }
.btn-detail { background: #faf5ff; color: #6b46c1; }
.btn-detail:hover { background: #e9d8fd; }
.btn-delete { background: #fff5f5; color: #c53030; }
.btn-delete:hover { background: #fed7d7; }
.detail-row {
background: #f7fafc;
padding: 16px;
}
.detail-row pre {
background: #edf2f7;
padding: 12px;
border-radius: 4px;
font-size: 13px;
overflow-x: auto;
}
.empty {
text-align: center;
padding: 30px;
color: #a0aec0;
}
</style>
六、本篇小结
| 知识点 | 核心内容 | 关键要点 |
|---|---|---|
| 组合式 API | 按功能聚合逻辑,替代选项式 API | 更好的代码组织、逻辑复用、TS 支持 |
| setup 函数 | 在 beforeCreate 之前执行,不能访问 this | setup 返回数据暴露给模板 |
<script setup> |
语法糖,自动暴露顶层绑定 | 本系列默认使用此方式 |
| ref | 任意类型的响应式引用 | 需要 .value 访问 |
| reactive | 对象的响应式代理 | 不能整体替换,解构丢失响应式 |
| computed | 派生状态,有缓存 | 只读/可写两种形式 |
| watch | 侦听数据变化,执行副作用 | 支持 deep、immediate 选项 |
| toRef / toRefs | 保持 reactive 解构后的响应式 | 用于组合式函数返回 |
| readonly | 只读代理,防止外部修改 | 保护内部状态 |
| shallowRef / shallowReactive | 浅层响应式,优化性能 | 只关心根级别变化 |
七、思考与练习
-
基础练习 :将上一节的数据表格组件中的筛选逻辑提取为一个组合式函数
useTableFilter,包含search、sort、filter等逻辑。 -
实践任务 :使用
readonly保护用户列表数据,只暴露只读版本,并提供addUser、updateUser、deleteUser三个方法来修改数据。 -
陷阱排查:分析以下代码的问题,并给出修正方案:
vue<script setup> import { reactive } from 'vue' const state = reactive({ count: 0 }) const { count } = state function increment() { count++ } </script> -
深度思考 :为什么
ref对原始值也能实现响应式,而reactive不能?其底层实现有什么不同?
下篇预告
第 12 篇我们将深入学习组合式 API 的高级应用------Composables(组合式函数):
- 如何封装和复用逻辑
- 组合式函数的设计原则
- 与 Vue 2 mixins 的对比
- 常见的组合式函数模式(useFetch、useLocalStorage、useDebounce 等)
准备好把逻辑复用的能力提升到新高度!🧩