3、Composition API 入门:setup 函数、ref 与 reactive 响应式基础
好,咱们正式开始啃 Composition API 这块硬骨头。
说实话,我刚从 Options API 切换到 Composition API 时,心里也犯嘀咕——这玩意儿到底好在哪?但用了一段时间后,我只能说:真香。尤其是当你面对一个几百行的组件时,Composition API 那种「按逻辑组织代码」的方式,简直是一种解脱。
3.1 setup 函数:一切从这里开始
在 Vue3 里,setup 是 Composition API 的入口。你可以把它理解成「组件的大脑」——所有逻辑、状态、方法,都在这里初始化。
核心要点:
setup在组件创建之前执行,此时this不可用- 它接收两个参数:
props和context - 返回的对象会暴露给模板使用
来看个最简单的例子:
// 基础 setup 函数
export default {
setup(props, context) {
// 这里不能使用 this
console.log(props.msg) // 可以访问 props
console.log(context.attrs) // 非 props 属性
// 返回给模板
return {
message: 'Hello Vue3!'
}
}
}
嗯,这里要注意:setup 里千万别用 this。我刚开始迁移时踩过这个坑,在 setup 里写了个 this.$emit,结果控制台报错半天没反应过来。后来才意识到,setup 执行时组件实例还没创建完呢。
3.2 ref:让普通数据变「活」
在 Vue2 里,我们习惯在 data 里定义响应式数据。到了 Vue3,ref 就是干这个活的——它能把一个普通值变成响应式。
我的习惯:对于基本类型(字符串、数字、布尔值),我几乎都用 ref。简单直接,不绕弯子。
import { ref } from 'vue'
export default {
setup() {
// 定义一个响应式计数器
const count = ref(0)
// 修改值要用 .value
function increment() {
count.value++
}
return {
count,
increment
}
}
}
你可能会问:为什么模板里不用写 .value?
因为 Vue3 在模板编译时自动解包了 ref。说白了,这是框架帮你做了一层「语法糖」。但在 setup 里,你必须手动加 .value——这个区别我一开始老忘。
避坑指南:我曾经在 setup 里直接写 count = 1,结果页面死活不更新。记住:ref 的值必须通过 .value 修改,否则不会触发响应式更新。
3.3 reactive:处理复杂对象的利器
如果说 ref 是「单兵作战」,那 reactive 就是「集团军」——它专门处理对象、数组这类复杂结构。
import { reactive } from 'vue'
export default {
setup() {
// 定义一个响应式对象
const user = reactive({
name: '张三',
age: 25,
hobbies: ['编程', '读书']
})
// 直接修改属性,不需要 .value
function updateName(newName) {
user.name = newName
}
return {
user,
updateName
}
}
}
你看,用 reactive 时,修改属性就像操作普通对象一样自然。不需要 .value,也不需要 $set——Vue3 底层用 Proxy 实现了真正的「深层响应」。
关键区别:
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型、对象 | 对象、数组 |
| 访问方式 | 需要 .value | 直接访问 |
| 解包行为 | 模板自动解包 | 深层响应 |
| 重新赋值 | 支持 | 不支持(会丢失响应) |
说到重新赋值,我遇到过这么个情况:用 reactive 定义了一个表单对象,后来想整个替换它,直接写了 form = newForm。结果页面没反应。为什么?因为 reactive 返回的是代理对象,你重新赋值相当于把代理丢了。
我的建议:如果你需要整体替换一个响应式对象,要么用 ref 包裹它,要么用 Object.assign 逐个属性更新。
3.4 ref 与 reactive 的选择策略
说实话,这两个 API 怎么选,没有绝对标准。但我个人有个「三原则」:
- 基本类型用 ref——简单、直观、不会出错
- 复杂对象用 reactive——省去 .value 的麻烦
- 需要整体替换时用 ref——避免丢失响应式
你想想看,如果整个组件都用 ref,代码里到处都是 .value,看着多累。反过来,如果所有数据都用 reactive,遇到基本类型又得包一层对象,也不优雅。
// 实际项目中的混合使用
setup() {
// 基本类型用 ref
const loading = ref(false)
const pageSize = ref(20)
// 复杂对象用 reactive
const tableData = reactive({
list: [],
total: 0,
currentPage: 1
})
// 需要整体替换的用 ref 包裹对象
const formData = ref({
name: '',
age: null
})
// 修改时
loading.value = true
tableData.list = response.data
formData.value = { name: '李四', age: 30 }
return {
loading,
pageSize,
tableData,
formData
}
}
嗯,这样混着用,代码反而更清晰。每个变量一看就知道它是什么类型、该怎么操作。
3.5 响应式原理:Proxy 到底做了什么?
你可能好奇:Vue3 的响应式为什么比 Vue2 强?
说白了,Vue2 用的是 Object.defineProperty,它只能拦截属性的「读取」和「赋值」。新增属性、删除属性、数组索引操作——这些它都管不了。所以 Vue2 才需要 $set、$delete 这些补丁。
Vue3 换成了 Proxy,它直接代理整个对象。任何操作——读取、赋值、新增、删除、甚至 in 操作符——都能被拦截到。
// 简单模拟 reactive 的原理
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 收集依赖
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
// 触发更新
trigger(target, key)
return result
}
})
}
你看,代码其实不复杂。核心就是「拦截操作 → 收集依赖 → 触发更新」这个流程。我在项目中曾经自己写过一个简化版的响应式系统,虽然只有几十行代码,但跑通的那一刻,对 Vue3 的理解直接上了一个台阶。
注意:虽然 Proxy 很强大,但它不能代理基本类型。这就是为什么 Vue3 还需要 ref——ref 内部其实是用一个对象包裹了基本类型,然后再用 reactive 处理这个包裹对象。
3.6 实战:一个完整的计数器组件
最后,咱们把今天学的串起来,写个完整的例子:
<template>
<div>
<h2>计数器:{{ count }}</h2>
<p>用户信息:{{ user.name }},{{ user.age }}岁</p>
<button @click="increment">加一</button>
<button @click="reset">重置</button>
</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
setup() {
// ref 处理基本类型
const count = ref(0)
// reactive 处理对象
const user = reactive({
name: '王五',
age: 28
})
function increment() {
count.value++
user.age++
}
function reset() {
count.value = 0
user.age = 28
}
return {
count,
user,
increment,
reset
}
}
}
</script>
这个例子虽然简单,但涵盖了 setup、ref、reactive 的核心用法。你可以在项目中先从小功能开始尝试,慢慢就会找到感觉。
下一章,咱们聊聊 computed 和 watch——这两个在 Composition API 里也变了不少。到时候我会分享一些我在实际项目中遇到的「坑」和「妙用」。