Vue 3 组合式 API 完全指南
本文档从零开始讲解 Vue 3 的组合式 API(Composition API):为什么需要它、setup 与 、ref 与 reactive、computed 与 watch、生命周期、模板 ref、provide/inject,以及组合式函数(Composables)的写法与示例,适合新手系统学习。
一、什么是组合式 API?为什么要有它?
1.1 选项式 API 的局限
在 Vue 2 和 Vue 3 的选项式 API 里,一个组件会拆成 data、methods、computed、mounted 等好几块:
export default {
data() {
return { count: 0, name: '' }
},
methods: {
add() {
this.count++
}
},
mounted() {
console.log(this.count)
}
}
当组件变大时,同一块逻辑会散落在不同选项里:和“用户”相关的 data、methods、mounted 混在和“列表”相关的代码中间,按“逻辑”组织代码比较难,复用也主要靠混入(mixins),混入又有命名冲突、来源不清晰等问题。
1.2 组合式 API 在做什么?
组合式 API 让你把与同一功能相关的数据、计算、监听、生命周期写在一起,而不是按 data/methods 拆开。
同时,这些逻辑可以抽成 “组合式函数”(如 useUser、useMouse),在多个组件里复用,且没有混入的合并问题。
组合式 API 的核心包括:
- ref、reactive:定义响应式数据。
- computed、watch:派生与监听。
- onMounted、onUnmounted 等:生命周期钩子。
- setup 或 :写“组合”逻辑的地方。
下面从 setup 和 开始讲,再讲 ref/reactive、computed/watch、生命周期、组合式函数。
二、setup 函数(组合的“入口”)
2.1 基本写法(不用 时)
组件可以导出一个 setup 函数。setup 在组件创建时执行,在 data、computed 等之前;你在这里定义的数据、方法等,通过 return 暴露给模板和其余选项使用。
export default {
setup() {
const count = ref(0)
function add() {
count.value++
}
return { count, add }
}
}
模板里就可以用 count 和 add。
setup 还可以接收两个参数:
- props:当前组件的 props(响应式,不要解构)。
- context:包含 attrs、emit、slots 等。
export default {
setup(props, context) {
console.log(props.title)
context.emit('update')
return { }
}
}
实际开发中,更常用 ,不用手写 return,写法更简单。
三、(推荐写法)
3.1 是什么?
是 setup 的语法糖:
在 里写的顶层变量、函数、import 的组件,会自动暴露给模板使用,不用 return。
<template>
<div>
<p>{{ count }}</p>
<button @click="add">+1</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function add() {
count.value++
}
</script>
- count、add 都在顶层声明,模板里直接用。
- 在 script 里用 count 时要写 count.value(下一节说明)。
3.2 和普通 script 的区别
- :只能有一个,且里面的代码在 setup 阶段执行,顶层绑定自动暴露给模板。
- 普通 :可和 同时存在(例如用 export default 写组件名、inheritAttrs 等),但逻辑复用和模板数据一般放在 里。
下文示例若无特别说明,都默认用 。
四、ref:定义响应式数据
4.1 为什么需要 ref?
在普通 JavaScript 里,改一个变量不会自动更新界面。
Vue 需要“追踪”你在模板和逻辑里用到的数据,所以要用 ref(或 reactive)把数据包成响应式的。
4.2 基本用法
ref(初始值) 会返回一个响应式引用。
在 里读取和修改时要写 .value;在 里会自动解包,不用写 .value。
<template>
<p>{{ count }}</p>
<button @click="count++">+1</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function add() {
count.value++
}
</script>
- 模板里:count 即可,不能写 count.value。
- script 里:count.value 才是当前值,count.value++ 才会触发更新。
4.3 ref 可以包任何类型
- 数字、字符串、布尔:
ref(0)、ref('')、ref(true)。 - 对象、数组:
ref({ name: 'a' })、ref([])。
此时 .value 是对象/数组本身,改 xxx.value.name 或 xxx.value.push(1) 都会触发更新。
const user = ref({ name: '小明', age: 18 })
user.value.name = '小红'
const list = ref([])
list.value.push(1)
4.4 在模板里何时要 .value?
在 里,顶层的 ref 会自动解包,一般不写 .value。
但在表达式里若把 ref 放进对象或数组里,不会自动解包,例如:
<p>{{ obj.count }}</p>
若 obj 是 { count: ref(0) },这里 obj.count 仍然是 ref,需要写成 obj.count.value 或在 script 里用 reactive/toRefs 处理。
日常写法里,模板中直接用顶层 ref 即可。
五、reactive:响应式对象
5.1 基本用法
reactive(对象) 会把整个对象变成响应式的。
在 script 里直接改属性即可,不需要 .value;模板里也是直接写属性名。
<template>
<p>{{ state.name }} {{ state.age }}</p>
<button @click="state.age++">年龄+1</button>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
name: '小明',
age: 18
})
</script>
- state 本身不是 ref,所以不能写 state = xxx 整体替换(会丢响应性)。
- 只能改属性:state.name = ‘小红’、state.age++。
5.2 reactive 只接受对象类型
reactive 只适合对象(包括数组)。
传数字、字符串等会报警告,应改用 ref。
5.3 解构会丢失响应性
从 reactive 解构出来的变量不再是响应式的:
const state = reactive({ count: 0 })
const { count } = state
count++ // 改的是普通变量,界面不会更新
需要“拆开用”又保持响应式时,用 toRefs:
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'a' })
const { count, name } = toRefs(state)
这样 count、name 是 ref,改 count.value 或 name.value 会同步回 state,模板里用 count、name 即可(自动解包)。
六、ref 和 reactive 怎么选?
| 对比 | ref | reactive |
|---|---|---|
| 类型 | 任意类型 | 仅对象/数组 |
| 读写 | script 里要 .value | 直接改属性 |
| 整体替换 | 可以 xxx = ref(新值) | 不能整体替换,会丢响应性 |
| 解构 | 解构出来仍是 ref | 解构会丢响应性,需 toRefs |
简单建议:
- 单值(数字、字符串、布尔、一个数组、一个“根对象”)→ 用 ref。
- 一组强相关的字段(如表单 state)→ 用 reactive 也可以;若希望解构或传给别人用,可 reactive + toRefs,或直接用多个 ref。
七、computed:计算属性
7.1 基本用法
computed(() => 返回值) 会根据依赖自动计算并缓存;依赖不变就不会重算。
在 script 里用要 .value,在模板里自动解包。
<template>
<p>双倍:{{ double }}</p>
</template>
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
</script>
7.2 可写计算属性(get/set)
需要“既能读又能写”时,传对象,写 get 和 set:
const fullName = computed({
get() {
return firstName.value + ' ' + lastName.value
},
set(v) {
const [first, ...rest] = v.split(' ')
firstName.value = first
lastName.value = rest.join(' ')
}
})
详细用法见《Vue3计算属性.md》。
八、watch 与 watchEffect:监听
8.1 watch(数据源, 回调)
watch 用来在某个数据变化时执行逻辑(如请求接口、存本地)。
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
监听 reactive 的某个属性要用 getter:
const state = reactive({ name: '' })
watch(() => state.name, (newVal) => {
console.log(newVal)
})
可加 { deep: true } 深度监听对象,{ immediate: true } 立即执行一次。详见《Vue3监听属性.md》。
8.2 watchEffect(回调)
watchEffect 会立刻执行一次回调,并收集回调里用到的响应式数据作为依赖;之后任一代变化就再执行。
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => {
console.log(count.value)
})
不需要“旧值”、且希望“一进来就跑一次”时,用 watchEffect 较方便。
九、生命周期钩子
在组合式 API 里,生命周期用 onXxx 的形式按需注册:
| 选项式 API | 组合式 API |
|---|---|
| beforeCreate / created | 无,逻辑直接写在 setup 里 |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
示例:
<script setup>
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
console.log('挂载完成')
})
onUnmounted(() => {
console.log('即将卸载,可清理定时器、监听')
})
</script>
可写多个 onMounted,都会执行;一般把“和某块逻辑相关”的清理放在对应的 onUnmounted 里。
十、模板 ref:拿到 DOM 或子组件实例
10.1 在模板上写 ref=”xxx”
在 里给元素或组件加 ref=”变量名”,在 里用 ref(null) 声明同名变量,挂载后 变量.value 就是该 DOM 元素或组件实例。
<template>
<input ref="inputRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value?.focus()
})
</script>
- inputRef.value 在挂载后才是 DOM,所以一般在 onMounted 里用。
- 若用 v-if 控制显示,可能要在 nextTick 后再访问。
10.2 在 v-for 里用 ref
ref 在 v-for 里会得到数组(每个元素对应一项);若只关心一个列表的“容器”DOM,可给外层包一个元素并 ref 到该元素上。
十一、provide / inject:跨层级传数据
11.1 作用
provide 在祖先提供数据,inject 在任意后代注入,无需层层 props。
11.2 基本用法
祖先:
import { ref, provide } from 'vue'
const theme = ref('dark')
provide('theme', theme)
后代:
import { inject } from 'vue'
const theme = inject('theme')
inject 可以写默认值:inject(‘theme’, ‘light’)。
适合主题、语言、用户信息等“全局性”数据。详见《Vue3组件.md》。
十二、组合式函数(Composables):逻辑复用
12.1 什么是组合式函数?
组合式函数就是用组合式 API 写出来的、可复用的函数。
命名一般以 use 开头,如 useMouse、useFetch。
函数里用 ref、reactive、onMounted、watch 等,把“数据 + 方法”return 出去,组件里调用就能得到同样的能力。
12.2 示例:useCounter
// composables/useCounter.js
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
function add() {
count.value++
}
function minus() {
count.value--
}
return { count, add, minus }
}
组件里:
<template>
<p>{{ count }}</p>
<button @click="add">+1</button>
<button @click="minus">-1</button>
</template>
<script setup>
import { useCounter } from './composables/useCounter'
const { count, add, minus } = useCounter(10)
</script>
12.3 示例:useMouse(监听鼠标位置)
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
组件里:
<template>
<p>鼠标:{{ x }}, {{ y }}</p>
</template>
<script setup>
import { useMouse } from './composables/useMouse'
const { x, y } = useMouse()
</script>
12.4 示例:useFetch(请求列表)
// composables/useFetch.js
import { ref, onMounted } from 'vue'
import axios from 'axios'
export function useFetch(url) {
const data = ref(null)
const loading = ref(false)
const error = ref('')
async function run() {
loading.value = true
error.value = ''
try {
const res = await axios.get(url)
data.value = res.data
} catch (e) {
error.value = e.message || '请求失败'
} finally {
loading.value = false
}
}
onMounted(run)
return { data, loading, error, run }
}
组件里:
<template>
<p v-if="loading">加载中...</p>
<p v-else-if="error">{{ error }}</p>
<ul v-else>
<li v-for="item in data" :key="item.id">{{ item.name }}</li>
</ul>
</template>
<script setup>
import { useFetch } from './composables/useFetch'
const { data, loading, error, run } = useFetch('/api/list')
</script>
十三、与选项式 API 混用(了解即可)
同一组件可以同时有 和 选项式 data、methods 等。
组合式 API 的 ref 等会先合并,选项式里可通过 this 访问;但实际开发中更推荐统一用组合式 API,避免混用增加心智负担。
十四、常见问题与注意点
14.1 ref 在 script 里必须 .value
在 里,ref 是引用对象,读写都要 .value;模板里不用。
14.2 reactive 不能整体替换
state = reactive({ … }) 会断掉响应式;只能改属性。
若确实要“换一整份数据”,可考虑用 ref 包对象:state.value = { … }。
14.3 解构 reactive 要用 toRefs
const { count } = reactive({ count: 0 }) 里 count 不是响应式的;要用 toRefs 转成 ref 再解构。
14.4 生命周期在 setup 里按顺序注册
多个 onMounted 会按注册顺序执行;逻辑按“功能块”组织即可。
十五、组合式 API 速查表
| 需求 | API |
|---|---|
| 响应式数据(任意类型) | ref(初始值),script 里 .value |
| 响应式对象 | reactive(对象),直接改属性 |
| 解构 reactive 仍响应式 | toRefs(reactive对象) |
| 计算属性 | computed(() => 值) |
| 监听 | watch(源, 回调)、watchEffect(回调) |
| 挂载后 | onMounted(fn) |
| 卸载前清理 | onUnmounted(fn) |
| 模板 ref | ref(null),模板 ref=”同名” |
| 跨层级传值 | provide(key, value)、inject(key) |
| 逻辑复用 | 写 useXxx() 组合式函数,return { … } |
十六、学习建议
- 先熟练 ref(含 .value)和 的写法,能写一个带数据与方法的页面。
- 再练 reactive 和 toRefs,理解“解构会丢响应性”。
- computed、watch、onMounted/ onUnmounted 按需用;有需要再查《Vue3计算属性》《Vue3监听属性》。
- 逻辑复用优先写 useXxx 组合式函数,替代混入;多写几个 useCounter、useMouse、useFetch 类的例子,体会“按功能组织 + 复用”。
把本文档里的示例在项目里敲一遍、改一改,会掌握得更牢。祝你学习顺利。