创建日期:2026-09-08 | 最近更新:2026-09-08 基于 Vue 3.5.42;API 与行为以 cn.vuejs.org 为准。
Composition API 与 script setup:把「选项」变成「函数」
一句话:Composition API 不是「另一种写 state 的方式」,而是把 Vue 2 里散落在
data/computed/methods/watch各栏的同一份逻辑,收拢到一个函数上下文里,让「组件逻辑」能像普通函数一样抽取、复用、组合。<script setup>只是它的语法糖——让你少写return。
1. ref vs reactive:先记住这三条
ref | reactive | |
|---|---|---|
| 适合 | 基本类型、需要整对象替换的场景 | 深层对象/嵌套状态 |
| 取值 | count.value(模板里自动解包,不用写 .value) | state.x(无 .value) |
| 特性 | 传参/解构不丢响应性 | 解构会丢(要 toRefs) |
给 Vue2 熟手的直觉:
// data() 时代的字段,绝大多数换成 ref:
const count = ref(0)
count.value++
// 一整坨「配置对象」这类,才用 reactive:
const form = reactive({ name: '', age: 0 })
form.name = 'Lin'
- 模板里 ref 自动解包:
<p>{{ count }}</p>不用写count.value(只有顶层 ref 会解包;塞进reactive的嵌套 ref 也会解包,见篇 3)。 - reactive 对象解构会断:
const { name } = form拿到的不是响应式。要解构且保留响应,用toRefs(form)。 - ref 也能装对象:
ref({ a: 1 })内部会用reactive包裹value,obj.value.a深层也响应。所以不确定时用 ref 准没错。
2. script setup:少写一半样板
Vue 2 里你写 <script> export default { ... } </script>;Vue 3 主流是:
<script setup>
import { ref, computed } from 'vue'
// 顶层声明的都是「模板可见」的,不用 return
const count = ref(0)
const double = computed(() => count.value * 2)
</script>
<template>
<button @click="count++">{{ count }} × 2 = {{ double }}</button>
</template>
- 模板里能用顶层变量/函数;
import进来的组件/组合式函数也直接可用,无需注册。 - 每个 SFC 只能有一个
<script setup>(可与普通<script>并存做极少量的 Options 兜底,但不推荐)。 <script setup>里的组件是默认关闭的——props/attrs 不会自动透传给你在模板里自由访问,外部想调组件内部方法要显式defineExpose(见 §6)。
3. computed 与 watch 家族
computed:是「懒 + 缓存」的派生状态
const search = ref('')
const results = computed(() => filter(list.value, search.value)) // 依赖变了才重算
模板、别的 computed、watch 都能读 results.value。能 computed 就别手动 watch + 赋值——后者容易造成「多处同步状态」的意大利面。
watch 家族的三个选择
watch(source, (val, old) => {}, { deep: true }) // 精确监听:单个 ref / getter / 数组
watch([a, b], ([av, bv], [aov, bov]) => {}) // 多个来源
watchEffect(() => { doWith(state.x) }) // 自动收集:回调里读到啥就盯啥
watchPostEffect(() => { /* DOM 更新后 */ }) // 等同 old flush:'post'
| Vue2 | Vue3 |
|---|---|
watch: { x() {} }(默认浅) | watch(x, cb);对象要 { deep: true } |
immediate: true | 加选项 { immediate: true } 或直接用 watchEffect |
| 在组件销毁自动停 | 组件卸载自动停;跨组件/全局场景用 watchEffect 拿的 stop + onScopeDispose(篇 5) |
提醒:Vue2 里 watch 对象默认是「引用变化才触发」;Vue3 里
watch默认不 deep(reactive对象除外——直接 watch 一个 reactive 对象时它深),要深监听显式deep: true。
生命周期对照
| Vue 2 | Vue 3 Composition |
|---|---|
created | 直接在 setup 顶层写(同步执行即此时机) |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
errorCaptured | onErrorCaptured |
| —— | onActivated/onDeactivated(keep-alive) |
在 <script setup> 里这些 onXxx 不用从 options 里翻,直接 import { onMounted } from 'vue' 后调用。
4. 组件通信的新形状
props 与 emits:编译宏
<script setup>
const props = defineProps({
title: { type: String, required: true },
init: { type: Number, default: 0 },
})
// 用带默认值的写法更省:defineProps({ init: { default: 0 } }) → props.init
const emit = defineEmits(['update', 'save'])
</script>
defineProps/defineEmits是编译宏:不需要 import,写在<script setup>顶层。- TS 项目里更常见类型版:
defineProps<{ title: string; init?: number }>()(篇 5)。 - 旧的
props: { … }+this.$emit在 Options 写法里依然可用;但新组件用宏。
defineModel:v-model 的时代终于不用手搓了
Vue2 里给组件做 v-model 要 value prop + this.$emit('input');Vue3 早期要 modelValue + update:modelValue。defineModel(3.4+)一键收编:
<!-- 父:<MyInput v-model="keyword" /> -->
<script setup>
const model = defineModel({ type: String, required: true })
// model 是个 ref:改它就相当于触发 update:modelValue
</script>
<template><input v-model="model" /></template>
多个 model:const a = defineModel('a'); const b = defineModel('b') → 父可写 v-model:a、v-model:b。加修饰符也一样(defineModel 会带 modifiers)。
暴露内部方法:defineExpose
<script setup> 组件默认「关着门」——父组件 ref 到它时拿不到内部函数。要开:
<script setup>
const doReset = () => { /* … */ }
defineExpose({ doReset }) // 父组件 ref 就能调用
</script>
attrs / slots 访问
import { useAttrs, useSlots } from 'vue'
const attrs = useAttrs() // 等价 this.$attrs(Vue3 里含监听器)
const slots = useSlots() // 等价 this.$slots
5. provide / inject:跨层传「活」的东西
Vue2 你见过 provide/inject 但不常用来传响应式。Vue3 里它俩是组合式跨层共享的主力(替代部分「到处用 Vuex」的冲动):
// 祖先组件
import { ref, provide } from 'vue'
const theme = ref('light')
provide('theme', theme) // 传的是 ref,后代改主题全家响应
provide('setTheme', v => theme.value = v)
// 任意后代组件
const theme = inject('theme')
- 传
ref而非原始值,才能「改一处、处处响应」; - 用组合式函数 + provide/inject 即可实现「无状态库的全局 store」——Pinia 本质就是这个模式 + 性能优化(篇 5)。
6. 什么时候还想用 Options
Vue 3 允许混用:一个 SFC 里可以 export default { setup() {...}, data() {...} }(Composition 为主体、少量 Options 兜底),但对新代码不推荐混。Options 唯一还有存在感的场景是老代码渐进迁移期——先不动的组件保持 Options,新增逻辑用 setup() 选项一点一点迁。
7. 常见的「从 Vue2 惯性」坑
- 忘了
.value:count++改成count.value++;模板里没事,<script>/JS 里忘写.value是最常见编译/运行 bug。 - 解构 reactive 丢响应:别
const { a } = reactive({...}),要toRefs或直接对 ref。 - watch 默认不深:深层对象变化没触发,先怀疑没写
deep: true。 - 把
this当真:Composition 里没有组件this;要拿实例相关能力用getCurrentInstance()(尽量少用)。 - 在
<script setup>外用了宏:defineProps只在<script setup>顶层有效,普通<script>里要用props选项。
关联
- 上一篇:Vue2 → Vue3 快车道
- 下一篇:迁移深水区
- 想深入「ref 为什么要有
.value、reactive 内部怎么工作」→ 响应式原理