第2章 2.1 Composition API 基础

🎯 开场:为什么你迟早要学会 Composition API?

上 一章我们学会了用「自定义事件」在组件之间传递消息,就像在公司里用内部邮件系统——能跑,但每次跨部门沟通都要走流程,文件一多就乱。

你有没有遇到过这些崩溃时刻?

  • 同一个逻辑散落在 datamethodscomputedwatch 四个地方,改一个问题要翻四个文件夹
  • 写一个功能,要在一个巨大的 methods 里翻半小时才能找到对应的代码
  • 接手别人的项目,满屏 this,完全不知道数据是怎么流动的

学完这章,你能:

用一个「把所有相关代码聚在一起」的方式写组件,逻辑再复杂也能一眼看明白。


🧱 基础:4 个核心概念,用生活场景讲清楚

2.1.1 setup:组件的「初始化房间」

是什么?

setup 是一个特殊的函数,它是 Vue3 组件的「初始化房间」——所有变量、函数、计算属性,在组件正式登场之前,先在这里准备好。
\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n
为什么要用?

以前的 data()methodscomputed 是四个独立的抽屉,你的东西可能散落在不同抽屉里。setup 就像一个「一站式准备台」,所有准备工作在一个地方搞定。

怎么用?

// App.vue
<script>
import { ref, reactive } from 'vue'

export default {
setup() {
// 👇 这里放所有组件需要的数据和函数
const name = '小明'
const age = ref(18)

const sayHello = () => {
  console.log(`你好,我是${name},今年${age.value}岁`)
}

// 👇 setup 必须返回一个对象,把需要暴露给模板的东西交出去
return {
  name,
  age,
  sayHello
}
}
}
</script>

这行在干嘛:

  • ref(18) 把普通数字变成「响应式数据」,像给物品贴追踪标签,值变了 Vue 知道
  • age.value 是「取标签里面的东西」,18 是标签内的实际数值
  • return出去的属性,模板里才能用

2.1.2 ref 和 reactive:两种「收纳术」

生活类比: 想象你搬家后整理行李箱。

ref:单独收纳贵重物品

每个 ref 就像一个小盒子,专门放一样东西(数字、字符串、布尔值)。

import { ref } from 'vue'

const count = ref(0)       // 盒子A:装了数字 0
const title = ref('购物清单')  // 盒子B:装了文字
const isDone = ref(false)  // 盒子C:装了是否完成

// 👇 拿东西出来要 .value
console.log(count.value)   // 输出: 0

// 👇 改东西也要 .value
count.value = 5
console.log(count.value)   // 输出: 5

reactive:把一堆相关东西打包成收纳袋

如果你的数据是一组相关的(比如一个人的信息),用 reactive 打个包,像超市的透明收纳袋,一眼能看到里面所有东西。

import { reactive } from 'vue'

// 👇 一个用户的所有信息打包在一起
const user = reactive({
name: '小明',
age: 18,
city: '北京'
})

// 👇 拿/改东西不用 .value,直接用
console.log(user.name)     // 输出: 小明
user.age = 20              // 直接改
console.log(user.age)      // 输出: 20

什么时候用哪个?

场景 用 ref 用 reactive
单个值(数字/文字/布尔)
一组相关数据(对象/数组)

⚠️ 注意: reactive 不支持基本类型,ref 支持所有类型。


2.1.3 computed:自动计算的「加工站」

是什么?

computed 是一个「加工站」——你把原料(已有响应式数据)送进去,它自动产出成品。只要原料变了,成品自动更新,不用你手动刷新。

为什么要用?

假设购物车里有商品和数量,你想实时知道「总价」。用手写函数每次调用都要算,用 computed 就像装了「自动计价器」,商品或数量变了,总价自动重新算。

怎么用?

import { ref, computed } from 'vue'

const price = ref(100)   // 单价
const quantity = ref(3)  // 数量

// 👇 加工站:自动计算总价
const total = computed(() => {
return price.value * quantity.value
})

console.log(total.value)  // 输出: 300

// 👇 改原料,加工站自动重算
price.value = 150
console.log(total.value)  // 输出: 450(自动更新!)

2.1.4 watch:盯着数据变化的「监控摄像头」

是什么?

watch 就像监控摄像头,当它盯着的数据发生变化时,自动执行你设定的回调函数(比如发请求、存数据库、弹提示)。

为什么要用?

当你想「某个数据变了,我就做某件事」,比如用户输入框内容变了就自动保存、计数器超过 10 就弹「太棒了」。

怎么用?

import { ref, watch } from 'vue'

const count = ref(0)

// 👇 摄像头盯着 count,变了就执行回调
watch(count, (newValue, oldValue) => {
console.log(`计数器从 ${oldValue} 变成了 ${newValue}`)

if (newValue >= 10) {
console.log('🎉 达到目标了!')
}
})

// 👇 改变量,触发监控
count.value = 5   // 控制台: 计数器从 0 变成了 5
count.value = 12  // 控制台: 计数器从 5 变成了 12,再输出: 🎉 达到目标了!

🔥 实战:3 个项目,从抄到跑通

项目 1:计数器(5 分钟)

需求: 一个带加减按钮的计数器,归零时提示「归零了」。

import { ref, watch } from 'vue'

// 初始化计数器
const count = ref(0)

// 盯着计数器,归零时提示
watch(count, (newVal) => {
if (newVal === 0) {
console.log('归零了,从头开始!')
}
})

// 👇 操作函数
const add = () => count.value++
const subtract = () => count.value--
const reset = () => count.value = 0

// 测试运行
add()      // count = 1
add()      // count = 2
subtract() // count = 1
reset()    // 控制台输出: 归零了,从头开始!

console.log(`最终计数: ${count.value}`) // 输出: 最终计数: 0

预期输出:

归零了,从头开始!
最终计数: 0

一句话解释: ref(0) 创建响应式计数器,watch 监控归零时刻。


项目 2:待办清单(15 分钟)

需求: 从 JSON 数据读取初始待办,支持添加、标记完成、筛选显示。

import { ref, computed, reactive } from 'vue'

// 初始待办数据(假设从服务器获取)
const initialTodos = [
{ id: 1, text: '买菜', done: false },
{ id: 2, text: '做饭', done: true },
{ id: 3, text: '洗碗', done: false }
]

// 用 reactive 存储待办列表(因为是数组)
const todos = reactive(initialTodos)

// 当前筛选:all / active / done
const filter = ref('all')

// 计算属性:自动筛选待办
const filteredTodos = computed(() => {
if (filter.value === 'active') {
return todos.filter(t => !t.done)
} else if (filter.value === 'done') {
return todos.filter(t => t.done)
}
return todos  // 'all' 返回全部
})

// 添加新待办
const addTodo = (text) => {
const newId = todos.length > 0 
? Math.max(...todos.map(t => t.id)) + 1 
: 1
todos.push({ id: newId, text, done: false })
}

// 切换完成状态
const toggleTodo = (id) => {
const todo = todos.find(t => t.id === id)
if (todo) todo.done = !todo.done
}

// ===== 测试运行 =====
console.log('=== 所有待办 ===')
filteredTodos.value.forEach(t => {
console.log(`${t.done ? '✅' : '⬜'} ${t.text}`)
})

addTodo('遛狗')
console.log('\n=== 添加"遛狗"后 ===')
filteredTodos.value.forEach(t => {
console.log(`${t.done ? '✅' : '⬜'} ${t.text}`)
})

toggleTodo(1)
console.log('\n=== 标记"买菜"完成后 ===')
filteredTodos.value.forEach(t => {
console.log(`${t.done ? '✅' : '⬜'} ${t.text}`)
})

filter.value = 'active'
console.log('\n=== 只看进行中 ===')
filteredTodos.value.forEach(t => {
console.log(`${t.done ? '✅' : '⬜'} ${t.text}`)
})

预期输出:

=== 所有待办 ===
⬜ 买菜
✅ 做饭
⬜ 洗碗

=== 添加"遛狗"后 ===
⬜ 买菜
✅ 做饭
⬜ 洗碗
⬜ 遛狗

=== 标记"买菜"完成后 ===
✅ 买菜
✅ 做饭
⬜ 洗碗
⬜ 遛狗

=== 只看进行中 ===
⬜ 洗碗
⬜ 遛狗

一句话解释: reactive 存列表,computed 自动筛选,ref 控制筛选状态。


项目 3:个人消费记录器(15 分钟)

需求: 读取消费记录,计算各类别支出占比,找出超支预警。

import { ref, computed, reactive } from 'vue'

// 模拟从 CSV 或 API 获取的消费数据
const expenseData = [
{ id: 1, category: '餐饮', amount: 45, date: '2024-01-15' },
{ id: 2, category: '交通', amount: 12, date: '2024-01-15' },
{ id: 3, category: '餐饮', amount: 38, date: '2024-01-16' },
{ id: 4, category: '购物', amount: 200, date: '2024-01-16' },
{ id: 5, category: '餐饮', amount: 55, date: '2024-01-17' },
{ id: 6, category: '娱乐', amount: 80, date: '2024-01-17' }
]

// 每月预算(可配置)
const budget = ref(500)

// 用 reactive 存储消费记录
const expenses = reactive(expenseData)

// 计算总支出
const totalExpense = computed(() => {
return expenses.reduce((sum, e) => sum + e.amount, 0)
})

// 按类别分组统计
const categoryStats = computed(() => {
const stats = {}
expenses.forEach(e => {
if (!stats[e.category]) {
  stats[e.category] = 0
}
stats[e.category] += e.amount
})
return stats
})

// 超支预警
const isOverBudget = computed(() => {
return totalExpense.value > budget.value
})

// 剩余预算
const remaining = computed(() => {
return budget.value - totalExpense.value
})

// 添加消费记录
const addExpense = (category, amount) => {
const newId = expenses.length > 0 
? Math.max(...expenses.map(e => e.id)) + 1 
: 1
const today = new Date().toISOString().split('T')[0]
expenses.push({ id: newId, category, amount, date: today })
}

// ===== 输出报告 =====
console.log('========== 消费报告 ==========')
console.log(`总支出: ¥${totalExpense.value}`)
console.log(`预算: ¥${budget.value}`)
console.log(`剩余: ¥${remaining.value}`)
console.log(`\n超支预警: ${isOverBudget.value ? '⚠️ 已超支!' : '✅ 未超支'}`)

console.log('\n--- 各类别支出 ---')
Object.entries(categoryStats.value).forEach(([cat, amount]) => {
const percent = ((amount / totalExpense.value) * 100).toFixed(1)
console.log(`${cat}: ¥${amount} (${percent}%)`)
})

// 测试添加一笔消费
addExpense('医疗', 150)
console.log('\n--- 添加医疗费 ¥150 后 ---')
console.log(`总支出: ¥${totalExpense.value}`)
console.log(`超支预警: ${isOverBudget.value ? '⚠️ 已超支!' : '✅ 未超支'}`)

预期输出:

========== 消费报告 ==========
总支出: ¥430

预算: ¥500
剩余: ¥70

超支预警: ✅ 未超支

--- 各类别支出 ---
餐饮: ¥138 (32.1%)
交通: ¥12 (2.8%)
购物: ¥200 (46.5%)
娱乐: ¥80 (18.6%)

--- 添加医疗费 ¥150 后 ---
总支出: ¥580
超支预警: ⚠️ 已超支!

一句话解释: computed 自动汇总统计,reactive 存储可变数据,二者配合实现实时仪表盘。


💪 进阶:5 个新人必踩的坑

❌ 坑1:忘了 .value

// ❌ 错误:ref 的值不用 .value 拿
const count = ref(0)
console.log(count)       // 输出: Ref<0>,不是 0!

// ✅ 正确:ref 的值要加 .value
console.log(count.value)  // 输出: 0

❌ 坑2:reactive 解构丢失响应式

// ❌ 错误:reactive 对象解构后变成普通值
const user = reactive({ name: '小明', age: 18 })
const { name, age } = user
name = '老王'        // 这个改法无效,视图不会更新!

// ✅ 正确:保持引用,或者用 toRefs
const user = reactive({ name: '小明', age: 18 })
// 方案1:不解构,直接用
console.log(user.name)

// 方案2:用 toRefs 转换后再解构
import { toRefs } from 'vue'
const { name, age } = toRefs(user)
name.value = '老王'   // 现在有效了

❌ 坑3:computed 里面改其他响应式数据导致死循环

// ❌ 错误:computed 里改 ref,会触发 computed 重新计算 → 死循环
const count = ref(0)

const doubled = computed(() => {
count.value = count.value * 2  // 别这样做!
return count.value
})

// ✅ 正确:computed 只做计算,不要有副作用
const count = ref(0)
const doubled = computed(() => {
return count.value * 2  // 只 return,不改东西
})

❌ 坑4:watch 监听 reactive 对象时用箭头函数参数

// ❌ 错误:reactive 对象不要解构后监听
const user = reactive({ name: '小明', age: 18 })
watch({ ...user }, (newVal) => {  // 这里解构了,丢掉了响应式
console.log('变了')
})

// ✅ 正确:直接监听 reactive 对象
const user = reactive({ name: '小明', age: 18 })
watch(user, (newVal) => {
console.log('用户信息变了:', newVal)
})

// 或者监听某个具体属性
watch(() => user.name, (newName) => {
console.log('名字改成:', newName)
})

❌ 坑5:setup 里误用 this

// ❌ 错误:setup 是箭头函数,没有 this
export default {
setup: () => {
const msg = '你好'
const show = () => {
  console.log(this.msg)  // this 是 undefined!
}
}
}

// ✅ 正确:setup 用普通函数,或者不用 this
export default {
setup() {  // 不用箭头
const msg = '你好'
const show = () => {
  console.log(msg)  // 直接用 closure
}
return { msg, show }
}
}

🛠️ 调试技巧:console.log 配合 computed 调试

import { computed } from 'vue'

const count = ref(10)
const doubled = computed(() => {
console.log('🔥 doubled 重新计算了')  // 加日志看什么时候触发
return count.value * 2
})

console.log(doubled.value)  // 触发一次计算
count.value = 20           // 改值
console.log(doubled.value)  // 又触发一次

✏️ 练习题

练习 1(2 分钟):计数器改上限
- 输入:count 初始值 0,上限 100
- 预期输出:加到 100 时提示「已达上限」
- 提示:把项目 1 的 add 函数加个判断就行

练习 2(2 分钟):待办清单加删除
- 输入:一个待办列表,删除 id=2 的项
- 预期输出:只剩 id=1 和 id=3 的待办
- 提示:用 todos.filter(t => t.id !== 2) 返回新数组,或用 splice

练习 3(3 分钟):消费记录按金额排序
- 输入:项目 3 的 expenses 数组
- 预期输出:按金额从大到小显示
- 提示:expenses.slice().sort((a, b) => b.amount - a.amount)

练习 4(5 分钟):给项目 2 加「待办数量统计」
- 输入:待办列表 [1,2,3] 全部完成,[4,5] 进行中
- 预期输出:已完成 3 项,进行中 2 项
- 提示:用两个 computed,分别 filter done === truedone === false

练习 5(3 分钟):分析这个报错
- 输入:代码 const name = ref('小明'); name = '老王'
- 预期输出:解释哪里错了,怎么改
- 提示:ref 的值要通过 .value 改,不能直接赋值


📝 作业:做一个「学习时间追踪器」

需求描述:

做一个记录每天学习时长的工具,帮你养成好习惯。

功能点:

  1. 记录每天各科目学习时长(分钟)
  2. 显示本周/本月总时长
  3. 超时预警(超过设定值提醒)
  4. 按科目统计占比

加分项:

  1. 数据持久化(存 localStorage)
  2. 可视化占比(用 emoji 进度条:✅✅✅⬜⬜

验收标准:

  • 能添加学习记录
  • 统计自动计算
  • 超时有提示

📚 总结

本文学了 3 件事:

  1. setup 是组件的「初始化中心」,所有响应式数据在这里注册
  2. ref 存单个值(要 .value),reactive 存一组相关值(直接用)
  3. computed 自动计算,watch 自动监控,二者配合实现「数据驱动」

延伸资源:

  • 📖 Vue3 官方文档 - Composition API
  • 📖 《Vue.js 3 实战》—— 梁灏著,配套源码适合跟着敲
  • 🎬 B站「技术胖」Vue3 教程—— 免费,语速适中

互动钩子:

你在项目里用过 watch 吗?遇到过「数据变了但页面没更新」的神奇 bug 吗?评论区聊聊,帮你分析!

下一章我们要揭开 Vue3「响应式」的秘密——为什么你改了个变量,模板就自动变了?背后的 Proxyref/reactive 是怎么配合工作的?学完那一章,你就能自己实现一个简化版 Vue3 了!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。