Vue script setup 中常见编译宏有哪些?
简化版
script setup 常见编译宏包括 defineProps、defineEmits、defineExpose、defineOptions、defineSlots、defineModel。它们不是运行时函数,而是编译阶段被 Vue SFC 编译器识别并转换的声明能力。
详细版
script setup 让组件声明更简洁,顶层变量可直接用于模板。
编译宏用于声明组件边界。
<script setup lang="ts">
const props = defineProps<{ title: string }>()
const emit = defineEmits<{ save: [id: number] }>()
function submit() {
emit('save', 1)
}
</script>
defineExpose 控制父组件通过模板 ref 能访问什么;defineOptions 声明组件选项;defineSlots 给插槽提供类型;defineModel 简化 Vue3.4 之后的 v-model 声明。面试重点是:它们是编译宏,不需要 import,也不能当普通函数随意动态调用。
完整版教学
一、script setup 是编译语法糖
普通 <script> 需要显式 export default 返回组件选项。
<script setup> 把组件 setup 逻辑直接写在顶层,编译器会把它转换成组件的 setup()。
模板可以直接访问顶层声明,不需要手动 return。
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
如果一个组件有 8 个模板变量,传统 setup 需要 return 8 个名字。
script setup 省掉这些机械代码,让组件更像“声明依赖和行为”。
二、编译宏不是普通运行时函数
defineProps、defineEmits 这些名字看起来像函数,但它们由 SFC 编译器在编译阶段处理。
通常不需要从 vue 中导入。
它们也不能放在普通函数、条件分支或循环里动态调用。
<script setup>
// 正确:顶层调用
const props = defineProps({ title: String })
// 错误思路:不要放进 if 或函数里动态声明
// if (flag) defineProps({ name: String })
</script>
| 宏 | 主要作用 | 常见场景 |
|---|---|---|
defineProps | 声明 props | 接收父组件参数 |
defineEmits | 声明事件 | 子传父 |
defineExpose | 暴露实例能力 | 父组件 ref 调子组件方法 |
defineModel | 声明 v-model | 表单组件双向绑定 |
理解“编译阶段”很重要。 它解释了为什么宏有位置限制,也解释了为什么最终浏览器里不一定存在这些函数调用。
三、defineProps 和 defineEmits 描述组件边界
组件边界最核心的是输入和输出。
defineProps 描述父组件能传什么;defineEmits 描述子组件会发出什么事件。
在 TypeScript 下,它们还能给调用方提供类型提示。
<script setup lang="ts">
const props = defineProps<{
userId: number
readonly?: boolean
}>()
const emit = defineEmits<{
select: [id: number]
close: []
}>()
</script>
如果团队有 20 个业务组件都按这种方式声明,调用方传错字段或事件参数时能更早暴露问题。 这比运行时点开页面才发现事件名写错要安全得多。
四、defineExpose 控制组件实例暴露面
script setup 默认是关闭式组件。
父组件通过模板 ref 不会自动拿到子组件内部所有变量。
如果确实要暴露方法,需要显式调用 defineExpose。
<script setup>
function focus() {
inputRef.value?.focus()
}
defineExpose({ focus })
</script>
未 expose:
父组件 ref -> 看不到内部实现
expose 后:
父组件 ref -> 只能访问公开方法 focus
这是一种封装边界。
组件内部有 15 个变量,但父组件只需要 focus 和 reset,就只暴露这两个。
暴露面越小,后续重构越安全。
五、defineSlots 和 defineModel 改善类型与双向绑定
defineSlots 主要服务 TypeScript 类型提示,让插槽参数更明确。
defineModel 则用于简化 modelValue + update:modelValue 这一套声明。
它们都是为了让组件 API 更紧凑、更可维护。
<script setup lang="ts">
const model = defineModel<string>()
defineSlots<{
default(props: { active: boolean }): any
}>()
</script>
在表单组件里,过去要同时写 prop 和 emit。
如果一个组件支持 3 个 model,样板代码会快速增加。
defineModel 能把常见双向绑定声明压缩到更直观的形式。
六、宏提升了声明清晰度,但不能滥用
script setup 的优势是简洁,不是让所有逻辑都堆在一个文件里。
当组件逻辑过长时,仍然应该提取 composable、子组件或工具函数。
编译宏负责边界声明,业务逻辑仍要按职责拆分。
记忆钩子:script setup 管写法变短,编译宏管组件边界变清楚。
面试回答可以先列宏,再强调“编译阶段、顶层调用、无需 import、不能动态调用”。 这几个限制比单纯背 API 更能说明你理解它。
七、常见误区与追问
- 误区:defineProps 需要从 vue 导入。 它是编译宏,通常不需要 import。
- 误区:编译宏可以在任意函数里调用。 它们应在
<script setup>顶层使用,不能像普通函数那样动态声明组件选项。 - 误区:script setup 会让父组件访问到所有内部变量。 默认不会,需要通过
defineExpose显式暴露。 - 追问:defineModel 解决了什么样板代码? 它简化
modelValue和update:modelValue的双向绑定声明。 - 追问:defineSlots 有什么价值? 它主要给插槽参数提供类型约束和编辑器提示。
- 追问:script setup 和普通 setup 能混用吗? 可以有普通
<script>处理少量模块级或选项逻辑,但日常应避免混乱。
八、加强记忆
这题按“写法、边界、限制”记。script setup 让顶层变量直达模板;defineProps/defineEmits 管输入输出,defineExpose 管实例暴露,defineSlots/defineModel 管类型化插槽和双向绑定。最后一定点出它们是编译宏,不是普通运行时函数。