
一、为什么要学 Composition API?
在以前我们写代码用Vue2写:
javascript
export default {
data() {
return { count: 0, msg: 'hello' }
},
methods: {
add() { this.count++ }
},
computed: {
double() { return this.count * 2 }
}
}
很明显

一个功能被拆成三块:data
、methods
、computed
,找起来麻烦,改起来更麻烦。
Vue3 提出了 Composition API,把同一功能的代码用函数包在一起,就像把袜子卷成球------整整齐齐不丢单只!
二、舞台:setup 函数
1. setup 是什么?
-
舞台 :所有 Composition API 都要在
setup()
里表演。 -
时机:组件创建前执行,比任何生命周期都早。
-
注意 :里面访问
this
会得到undefined
(因为组件实例还没出生呢)。
javascript
<template>
<p>{{ name }}</p>
<button @click="changeName">改名</button>
</template>
<script>
export default {
setup() {
let name = '张三'
function changeName() {
name = '李四' // 页面不会变!因为 name 不是响应式
console.log(name)
}
return { name, changeName } // 暴露给模板用
}
}
</script>
我说白了:setup
是 Vue 3 中一个新概念,它就像是组件的"控制中心"或"大脑"。让我用大白话给你解释:
想象一下你要组装一个玩具:
setup
就像是打开工具箱的步骤在这个工具箱里,你准备好所有零件(数据、方法、计算属性等)
然后你告诉 Vue:"这些就是我要在组件里使用的所有东西"
三、让数据活起来:ref 与 reactive
让数据活起来是啥意思呢?
让我们来试试:
javascriptlet A = 1
javascriptimport { ref } from 'vue' let B = ref(1)
这两有什么区别呢?
第一个是一个"死"数据,是非响应式的数据
第二个是可以发生更改的响应式数据
1. ref:基本类型的响应式法宝
-
语法 :
let xxx = ref(初始值)
-
规矩 :在 JS 里改要加
.value
,模板里直接用。
javascript
<script setup>
import { ref } from 'vue'
let age = ref(18)
function addAge() {
age.value++ // JS 里要加 .value
}
</script>
<template>
<p>年龄:{{ age }}</p> <!-- 模板里不用 .value -->
<button @click="addAge">+1</button>
</template>
2. reactive:对象类型的响应式法宝
-
语法 :
let obj = reactive({...})
-
深度响应:对象里随便嵌套多少层,都能追踪变化。
javascript<script setup> import { reactive } from 'vue' let car = reactive({ brand: '奔驰', price: 100 }) function addPrice() { car.price += 10 } </script> <template> <p>我的{{ car.brand }}价值{{ car.price }}万</p> <button @click="addPrice">再涨10万</button> </template>
3. ref vs reactive 速记表
场景 推荐 原因 基本类型 ref reactive 会报警告 简单对象 均可 看个人喜好 深层嵌套对象 reactive 写起来更简洁,无需层层.value 四、语法糖
<script setup>
:懒人必备每次写
setup() { return {...} }
很烦?Vue3 提供语法糖:
javascript
<script setup>
// 这里面的代码直接就是 setup 的内容,无需 return
import { ref } from 'vue'
const count = ref(0)
</script>
(还能更懒)
再装个插件 vite-plugin-vue-setup-extend
,还能直接写组件名:
javascript
<script setup name="UserCard">
// 组件名就叫 UserCard,省去写 name 属性
</script>
五、解构不丢响应式:toRefs & toRef
当你想从 reactive 对象中"摘"属性出来用时,直接解构会丢失响应式,就像把耳机线从卷线盒里抽出来------全乱了!
用 toRefs
/ toRef
解决:
哈这个时候就会有同学说,toRef与toRefs的区别是什么呢?,其实最大的区别就是有没有s
toRef
的作用
- 功能 :将响应式对象(reactive 创建的对象)中的单个属性转换为一个响应式的 ref 对象。
toRefs
的作用
- 功能 :将响应式对象(reactive 创建的对象)的所有属性批量转换为 ref 对象,并包装成一个普通对象(每个属性都是对应的 ref)。
javascript
import { reactive, toRefs } from 'vue'
const user = reactive({
name: 'Alice',
age: 20
})
// 将 user 的所有属性转换为 ref 并包装成普通对象
const refs = toRefs(user)
// refs 结构:{ name: Ref, age: Ref }
// 解构后仍保持响应性
const { name, age } = refs
// 修改 ref 会同步影响原对象
name.value = 'Bob'
console.log(user.name) // 输出:Bob
// 修改原对象也会同步影响 ref
user.age = 21
console.log(age.value) // 输出:21
好啦,到这里已经将最基础的几个用法,和框架已经搭好了,所以也来用两个练习来巩固一下下
六、实战小案例:待办事项
需求:添加、删除、标记完成,并统计完成数量。
javascript
<script setup>
import { ref, reactive, computed } from 'vue'
// 1. 数据
const todos = reactive([])
const input = ref('')
// 2. 计算属性
const doneCount = computed(() => todos.filter(t => t.done).length)
// 3. 方法
function addTodo() {
if (!input.value.trim()) return
todos.push({ id: Date.now(), text: input.value, done: false })
input.value = ''
}
function toggle(todo) {
todo.done = !todo.done
}
function delTodo(id) {
const index = todos.findIndex(t => t.id === id)
todos.splice(index, 1)
}
</script>
<template>
<h2>待办清单</h2>
<input v-model="input" @keyup.enter="addTodo" placeholder="输入后敲回车" />
<ul>
<li v-for="t in todos" :key="t.id">
<input type="checkbox" v-model="t.done" />
<span :class="{ done: t.done }">{{ t.text }}</span>
<button @click="delTodo(t.id)">删除</button>
</li>
</ul>
<p>已完成:{{ doneCount }} / {{ todos.length }}</p>
</template>
<style>
.done { text-decoration: line-through; color: gray; }
</style>
这里面有一个用法没讲,就是computed,它的作用也很简单,就是对数据的自动更新(当数据变化的时候,它这个函数就会触发,让数据发生更新)
七、课后作业(动手才能真会)
-
个人资料卡
用
<script setup>
做一个可编辑的"姓名、年龄、邮箱、简介"卡片,要求支持新增、修改、删除。 -
购物车 2.0
在上文购物车案例基础上,增加:
-
商品数量加减
-
优惠券打 9 折
-
按分类筛
-
因为我们现在是比较基础的,所以我们最好是逻辑清晰,将这些都理解
作业一:
思路:
卡片:很简单,用一个div盒子装着,将里面的元素全部写在盒子里面,再将里面的元素居中,样式肯定就不会差。
可编辑的"姓名、年龄、邮箱、简介":用input将他们显示出来,但是我们要获取输入框里面的元素,这个时候我们就需要v-model进行双向绑定
新增、修改、删除.:这些毫无疑问肯定是一堆按钮,所以我们需要写点击按钮,里面函数用一些的方法进行这些操作
最后其实需要显示出来的,将原本一开始的元素显示出来,后面增加的元素也需要事实显示出来。
代码(简陋版)
javascript
<script setup lang="ts">
// 练习1:创建一个个人资料编辑组件,要求使用setup语法糖,
// 包含姓名、年龄、邮箱、个人简介等字段,实现数据的增删改查功能。
import {reactive} from "vue";
import { ref } from 'vue'
interface Profile {
id: number
name: string
age: number
email: string
intro: string
}
// 资料列表
const profiles = ref<Profile[]>([])
// 表单数据
const form = ref<Profile>({
id: Date.now(),
name: '',
age: 0,
email: '',
intro: ''
})
function text() {
console.log(profiles)
console.log("=================")
console.log(form)
}
const isEdit = ref(false)
let editIndex = -1
function resetForm() {
form.value = {
id: Date.now(),
// id: Date.now() 表示用当前的时间戳(即自1970年1月1日以来的毫秒数)来生成一个唯一的数字,作为这个新表单的唯一标识。
name: '',
age: 0,
email: '',
intro: ''
}
isEdit.value = false
editIndex = -1
}
// ... 是扩展运算符,用于"展开"对象里的所有属性。
// { ...form.value } 表示"复制 form.value 里的所有属性到一个新对象"。
// 常用于【对象浅拷贝】、【合并属性】等操作。
// 等价于
// const obj = {
// name: form.value.name,
// age: form.value.age,
// // ...其他所有属性
// }
// 但用 ...form.value 写法更简洁、更灵活。
// 新增或保存编辑
function handleSubmit() {
if (isEdit.value && editIndex !== -1) {
profiles.value[editIndex] = { ...form.value }
} else {
profiles.value.push({ ...form.value })
}
resetForm()
}
// 编辑
function editProfile(idx: number) {
const item = profiles.value[idx]
form.value = { ...item }
isEdit.value = true
editIndex = idx
}
// 删除
function deleteProfile(idx: number) {
profiles.value.splice(idx, 1)
if (isEdit.value && editIndex === idx) {
resetForm()
}
}
</script>
<template>
<button @click="text">检查</button>
<div>
<h2>个人资料编辑</h2>
<form @submit.prevent="handleSubmit">
<div>
<label>姓名:</label>
<input v-model="form.name" required />
</div>
<div>
<label>年龄:</label>
<input v-model.number="form.age" type="number" required min="0" />
</div>
<div>
<label>邮箱:</label>
<input v-model="form.email" type="email" required />
</div>
<div>
<label>个人简介:</label>
<textarea v-model="form.intro" required></textarea>
</div>
<button type="submit">{{ isEdit ? '保存修改' : '添加' }}</button>
<button v-if="isEdit" type="button" @click="resetForm">取消编辑</button>
</form>
<h3>资料列表</h3>
<table border="1" cellpadding="5">
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>邮箱</th>
<th>个人简介</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(profile, idx) in profiles" :key="profile.id">
<!-- <td>{{ key }}</td>-->
<td>{{ profile.name }}</td>
<td>{{ profile.age }}</td>
<td>{{ profile.email }}</td>
<td>{{ profile.intro }}</td>
<td>
<button @click="editProfile(idx)">编辑</button>
<button @click="deleteProfile(idx)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<style scoped>
</style>
作业二:
接下来的购物车的思路:
其实完全就是名片作业的升级版而已,你将名片变成购物车,进行购物车的增加,删除功能
新功能:进行购物车里面商品的价钱的总和,并且进行优惠价的计算
javascript
<template>
<div class="shopping-cart">
<h2>购物车(进阶版)</h2>
<div class="category-filter">
<span>分类:</span>
<button
v-for="category in categories"
:key="category"
:class="{ active: selectedCategory === category }"
@click="selectCategory(category)"
>
{{ category }}
</button>
</div>
<table class="cart-table">
<thead>
<tr>
<th>商品</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in filteredCartItems" :key="item.id">
<td>{{ item.name }} ({{ item.category }})</td>
<td>¥{{ item.price.toFixed(2) }}</td>
<td>
<button @click="decreaseQuantity(item)">-</button>
{{ item.quantity }}
<button @click="increaseQuantity(item)">+</button>
</td>
<td>¥{{ (item.price * item.quantity).toFixed(2) }}</td>
<td><button @click="removeItem(item)">删除</button></td>
</tr>
</tbody>
</table>
<div class="coupon-section">
<label for="coupon">优惠券:</label>
<select id="coupon" v-model="selectedCoupon">
<option value="">请选择</option>
<option v-for="coupon in coupons" :key="coupon.id" :value="coupon.code">
{{ coupon.name }} ({{ coupon.discount }}% off)
</option>
</select>
</div>
<div class="total">总计:¥{{ total.toFixed(2) }}</div>
</div>
</template>
<script>
export default {
data() {
return {
cartItems: [
{
id: 1,
name: "苹果",
category: "生鲜",
price: 6.5,
quantity: 3
},
{
id: 2,
name: "T恤",
category: "服饰",
price: 59,
quantity: 1
},
{
id: 3,
name: "蓝牙耳机",
category: "数码",
price: 199,
quantity: 1
}
],
categories: ["All", "生鲜", "数码", "服饰"],
selectedCategory: "All",
coupons: [
{ id: 1, code: "COUPON1", name: "满减券", discount: 10 },
{ id: 2, code: "COUPON2", name: "折扣券", discount: 15 }
],
selectedCoupon: ""
};
},
computed: {
filteredCartItems() {
if (this.selectedCategory === "All") {
return this.cartItems;
}
return this.cartItems.filter(item => item.category === this.selectedCategory);
},
total() {
return this.cartItems.reduce((acc, item) => acc + item.price * item.quantity, 0);
}
},
methods: {
selectCategory(category) {
this.selectedCategory = category;
},
increaseQuantity(item) {
item.quantity++;
},
decreaseQuantity(item) {
if (item.quantity > 1) {
item.quantity--;
}
},
removeItem(item) {
const index = this.cartItems.indexOf(item);
if (index!== -1) {
this.cartItems.splice(index, 1);
}
}
}
};
</script>
<style scoped>
.shopping-cart {
font-family: Arial, sans-serif;
}
.category-filter button {
margin-right: 5px;
padding: 5px 10px;
cursor: pointer;
}
.category-filter button.active {
background-color: #4CAF50;
color: white;
}
.cart-table {
border-collapse: collapse;
width: 100%;
margin: 20px 0;
}
.cart-table th,
.cart-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.cart-table button {
padding: 2px 5px;
cursor: pointer;
}
.coupon-section {
margin: 10px 0;
}
.total {
font-weight: bold;
font-size: 1.2em;
}
</style>
基础的语法讲到这里就结束拉!
