文章目录
前言
Composition API 是 Vue 3 组织组件逻辑的核心方式。前几篇已涉及 ref、computed、Composables 等,本篇从语法与机制层面深入,讲清楚:
setup()与<script setup>的关系- 编译器宏的使用与限制
- setup 中没有
this的原因与替代方式 - reactive 解构与组件实例访问
一、setup 函数
1.1 执行时机
创建组件实例
↓
初始化 props
↓
setup(props, context) ← 在此执行
↓
beforeMount → mounted → ...
- 在 beforeCreate / created 之前执行,等价替代这两个钩子
- 此时实例尚未完全创建,没有
this
1.2 基本写法
<script>
import { ref, computed, onMounted } from 'vue'
export default {
props: { title: String },
setup(props, { emit, attrs, slots, expose }) {
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => {
count.value++
emit('update', count.value)
}
onMounted(() => {
console.log('mounted', props.title)
})
// 必须 return 才能在模板中使用
return {
count,
double,
increment
}
}
}
</script>
<template>
<p>{{ count }} × 2 = {{ double }}</p>
<button @click="increment">+1</button>
</template>
1.3 context 参数
| 属性 | 含义 |
|---|---|
attrs | 非 props 的属性(class、style、id 等) |
slots | 插槽内容 |
emit | 触发事件 |
expose | 显式暴露给父组件 ref 的内容 |
setup(props, { emit, expose }) {
const validate = () => true
expose({ validate }) // 等价于 defineExpose
return { /* ... */ }
}
二、script setup 语法糖
2.1 是什么
<script setup> 是 setup() 的编译时语法糖:顶层变量、函数、import 自动暴露给模板,无需 return。
<!-- 等价于上面的 setup + return -->
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
</script>
<template>
<p>{{ double }}</p>
<button @click="increment">+1</button>
</template>
2.2 对比
| 对比项 | setup() | <script setup> |
|---|---|---|
| return | 必须手动 return | 顶层自动暴露 |
| 组件名 | export default { name } | defineOptions({ name }) |
| props/emit | 选项或 setup 参数 | defineProps / defineEmits |
| 默认导出 | 需要 | 不需要,<script setup> 即组件 |
| 推荐度 | 老项目/特殊场景 | 新项目首选 |
2.3 与普通 script 共存
<script>
// 普通 script:仅执行一次,不能访问 setup 变量
export default {
inheritAttrs: false
}
</script>
<script setup>
const count = ref(0)
</script>
三、编译器宏
编译器宏在 <script setup> 中无需 import,编译阶段被处理,不能放在条件语句或函数内。
3.1 defineProps
<script setup>
// 运行时声明
const props = defineProps(['title', 'count'])
// 类型 + 默认值
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 }
})
// TypeScript
const props = defineProps<{
title: string
count?: number
}>()
</script>
3.2 defineEmits
<script setup>
const emit = defineEmits(['update', 'close'])
// 类型
const emit = defineEmits<{
update: [value: number]
close: []
}>()
const submit = () => emit('update', 1)
</script>
3.3 defineExpose
<script setup>
const formRef = ref(null)
const validate = () => { /* ... */ }
defineExpose({ validate, formRef })
</script>
父组件 childRef.value.validate() 只能访问 expose 的内容。
3.4 defineOptions(Vue 3.3+)
<script setup>
defineOptions({
name: 'UserList',
inheritAttrs: false
})
</script>
替代在普通 <script> 里写组件选项。
3.5 defineModel(Vue 3.4+)
<script setup>
// 等价于 modelValue + update:modelValue
const title = defineModel('title', { type: String, default: '' })
</script>
<template>
<input v-model="title" />
</template>
简化 v-model 多个绑定名的写法。
四、setup 中没有 this
4.1 为什么
setup 执行时组件实例尚未创建,不存在 this。这是刻意设计,避免 Options API 中 this 指向混乱。
setup() {
console.log(this) // undefined(严格模式下)
}
4.2 如何替代
| Options API | Composition API |
|---|---|
this.xxx 数据 | ref / reactive |
this.$props | defineProps / setup(props) |
this.$emit | defineEmits / emit |
this.$attrs | useAttrs() |
this.$slots | useSlots() |
this.$refs | ref() 模板引用 |
| 组件实例 | getCurrentInstance()(慎用) |
4.3 getCurrentInstance
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
// 开发调试可用,不推荐业务逻辑依赖
console.log(instance?.proxy) // 类似 this 的代理
</script>
注意:仅用于插件、底层库;业务代码应使用 props、emit、composables,避免依赖内部实例。
五、useAttrs 与 useSlots
5.1 useAttrs
<script setup>
import { useAttrs } from 'vue'
defineProps(['label'])
const attrs = useAttrs() // 除 props 外的属性(class、data-* 等)
</script>
<template>
<label>
{{ label }}
<input v-bind="attrs" />
</label>
</template>
配合 inheritAttrs: false 手动绑定 attrs 到内部元素。
5.2 useSlots
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
onMounted(() => {
console.log(slots.header?.())
})
</script>
<template>
<header v-if="slots.header">
<slot name="header" />
</header>
</template>
六、reactive 解构失去响应性
6.1 问题
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state
count++ // ❌ 不是响应式的,只是普通数字
解构得到的是当前值的拷贝,与 reactive 对象断开连接。
6.2 toRefs 解决
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)
count.value++ // ✅ 响应式
toRefs 把每个属性转成 ref,解构后仍保持响应性。
6.3 toRef 单个属性
const count = toRef(state, 'count')
// 或
const count = toRef(() => state.count)
6.4 props 解构
<script setup>
const props = defineProps(['title'])
// ❌ 失去响应性
const { title } = props
// ✅ 保持响应性
const { title } = toRefs(props)
// 或
const title = toRef(() => props.title)
// 或 Vue 3.5+ 响应式 props 解构(编译器支持)
</script>
七、逻辑组织方式
7.1 按功能分组
<script setup>
// ===== 搜索逻辑 =====
const keyword = ref('')
const results = ref([])
const search = async () => { /* ... */ }
// ===== 分页逻辑 =====
const page = ref(1)
const pageSize = ref(10)
const changePage = (p) => { page.value = p; search() }
// ===== 生命周期 =====
onMounted(search)
</script>
复杂组件按业务关注点组织,而非 Options API 的 data/methods 分散。
7.2 提取 Composable
// composables/useSearch.js
export function useSearch(fetchApi) {
const keyword = ref('')
const results = ref([])
const search = async () => { /* ... */ }
return { keyword, results, search }
}
详见《Mixins 与 Composables》篇。
八、与 Options API 混用
export default {
data() {
return { legacy: 1 }
},
setup() {
const modern = ref(2)
return { modern }
}
}
可以共存,但不推荐同一组件混用两种风格,维护成本高。新代码统一用 Composition API + <script setup>。
九、面试聚焦
9.1 setup 中没有 this
实例未创建,用 props、ref、emit 等显式 API 替代 this。
9.2 script setup 如何获取组件实例?
getCurrentInstance(),仅建议底层场景;业务用 props/emit/composables。
9.3 defineProps 为什么要编译器宏?
编译期提取 props 定义,生成运行时校验,且无需 import、性能更好;不能在运行时条件分支中使用。
9.4 reactive 解构为什么失去响应性?
解构取的是当前值快照;用 toRefs / toRef 转为 ref 保持引用。
十、易混淆点
- setup 替代 beforeCreate/created:不是替代 mounted。
- 编译器宏不能 import:defineProps 等是编译指令,非运行时函数。
- script setup 无 return:顶层绑定即模板可用。
- getCurrentInstance 仅 setup 同步阶段可靠:异步回调中可能为 null。
- ref 解构也失去响应性:需
storeToRefs(Pinia)或保持xxx.value。
十一、思考与练习
1. setup 和 script setup 的关系?
解析:script setup 是 setup 的语法糖,编译后等价于 setup 函数 + return 顶层绑定。
2. 为什么 setup 里没有 this?
解析:执行时组件实例尚未创建;鼓励显式 API,避免 this 指向问题。
3. defineExpose 的作用?
解析:script setup 默认封闭,defineExpose 指定父组件 ref 可访问的方法/属性。
4. reactive 解构如何解决响应性?
解析:使用 toRefs 将各属性转为 ref,解构后通过 .value 保持响应。
5. defineProps 能在 if 里调用吗?
解析:不能,编译器宏必须在 script setup 顶层同步调用。
总结
- setup:Composition API 入口,无 this,需 return(或 script setup 自动暴露)
- script setup:推荐写法,顶层变量即模板可用
- 编译器宏:defineProps / defineEmits / defineExpose / defineOptions / defineModel
- 实例访问:业务用 props、emit、useAttrs;getCurrentInstance 慎用
- 解构:reactive、props 解构需 toRefs / toRef 保持响应性
1414

被折叠的 条评论
为什么被折叠?



