面试题:Vue 3 <script setup> 的本质原理是什么?它与普通 setup() 有何区别?
💡 核心思路
<script setup> 不是运行时 API,而是 SFC(单文件组件)的编译器语法糖;它在编译阶段被转换为标准 Composition API 代码,运行时零开销,核心解决的是"样板代码冗余"与"类型推导困难"的问题。
🏗️ 解决方案架构图(文本版)
text
[源码层] <script setup> + defineProps/Emits
│
▼ (编译时 Compile Time)
[编译器] @vue/compiler-sfc
├── 1. 宏提取: 识别 define* 宏,生成 props/emits 选项对象
├── 2. 变量收集: AST 分析模板依赖,自动收集顶层绑定
├── 3. 组件注册: import 导入的组件自动挂载到 components
└── 4. 代码转换: 包裹进 setup() 函数,注入 __returned__
│
▼ (产物层 Output)
[标准JS] export default {
props: {...}, // 由 defineProps 生成
emits: [...], // 由 defineEmits 生成
components: {...}, // 由 import 自动生成
setup(__props, { emit }) {
// 用户编写的业务逻辑
const count = ref(0)
// 自动生成的返回对象 (包含模板用到的所有变量)
return { count, ...imports }
}
}
│
▼ (运行时 Runtime)
[渲染器] 无额外性能开销,等同于手写 setup
🔑 核心干货解析
1. 本质定义(纠正认知偏差)
- ❌ 错误理解: 是一种新的响应式 API 或运行时特性。
- ✅ 正确理解: 是 编译时语法糖 。它只在
.vue文件的编译阶段生效,浏览器运行的代码与普通setup()完全一致。 - 主要矛盾: 开发者编写体验(简洁性) vs 框架运行时性能(稳定性)。
<script setup>通过把复杂度转移到编译阶段,实现了开发体验提升而运行时零损耗。 - 次要矛盾: 魔法宏(Macros)的隐式行为 vs IDE 类型提示。需要依赖 Volar/vue-official 插件提供类型支持。
2. 五大核心便利(原理级解释)
| 特性 | 表面现象 | 底层编译原理 | 使用场景 |
|---|---|---|---|
| 自动暴露 | 无需 return | 编译器遍历 <template> AST,将引用的标识符自动加入 return 对象 |
所有 SFC 组件开发 |
| 宏 API | defineProps 等无需 import |
编译器正则/AST 匹配宏名称,提取参数转为组件 Options,运行时删除该函数调用 | 定义组件接口 |
| 异步组件 | 顶层 await |
编译器将 setup 标记为 async,配合 Suspense 使用 |
数据预加载、SSR |
| 自动注册 | import 即用 | 编译器检测 import 语句,若为组件则自动添加到 components 选项 |
模块化组件拆分 |
| 显式暴露 | defineExpose |
默认关闭实例访问,仅暴露指定属性,防止内部状态泄漏 | 父组件 ref 调用子组件方法 |
3. 关键补充
- ⚠️ 补充:
defineProps/defineEmits/defineExpose/defineOptions/defineSlots是 编译器宏(Compiler Macros) ,不是真实的 JavaScript 函数。禁止对其进行解构、重命名或动态传参(除非使用 TS 泛型写法)。 - ⚠️ 补充:
<script setup>中定义的变量默认对模板公开,但对父组件通过ref获取实例时是 完全私有 的(不同于 Options API),必须通过defineExpose显式暴露。这是为了安全封装。
4. 边界场景与注意事项
- 不能在非 SFC 中使用:
.ts/.js文件中无法使用<script setup>,只能用标准setup()函数。 - 不能与普通
<script>混用时的限制: 可以共存,但普通<script>只能用于:声明全局类型、执行副作用、导出非组件内容。组件选项(name/inheritAttrs)需用defineOptions宏(Vue 3.3+)。 - HMR 热更新:
<script setup>对 HMR 更友好,因为编译器能精确追踪依赖变化。 - Tree-shaking: 由于编译产物更精确,未使用的导入和变量更容易被打包工具剔除,体积通常比手写 setup 更小。
💻 示例代码对比
源码 (<script setup>):
vue
<script setup lang="ts">
import MyButton from './MyButton.vue' // 自动注册
const props = defineProps<{ msg: string }>() // 宏,无需import
const emit = defineEmits<{ change: [val: string] }>()
// 顶层 await (需 Suspense)
const data = await fetchData()
const handleClick = () => emit('change', 'clicked')
// 显式暴露
defineExpose({ handleClick })
</script>
<template>
<MyButton @click="handleClick">{{ msg }}</MyButton>
</template>
编译产物(简化示意):
js
import MyButton from './MyButton.vue'
import { resolveComponent as _resolveComponent } from 'vue'
export default {
props: { msg: { type: String, required: true } },
emits: ['change'],
components: { MyButton }, // 自动注册
async setup(__props, { expose, emit }) {
const data = await fetchData()
const handleClick = () => emit('change', 'clicked')
expose({ handleClick }) // defineExpose 转换
// 自动返回模板所需变量
return {
msg: __props.msg,
MyButton,
handleClick,
data
}
}
}
🏆 满分答案(面试话术)
面试官问: "讲一下
<script setup>的原理?"回答:
"
<script setup>本质上是 Vue 3 SFC 的 编译时语法糖 ,而非运行时特性。它的核心价值是在 不增加运行时开销 的前提下,大幅提升开发体验和类型推导能力。具体来说,编译器在构建阶段会做四件事:
第一,将
defineProps、defineEmits等 编译器宏 提取为组件的 props/emits 选项,并在运行时代码中移除这些函数调用;第二,通过 AST 静态分析模板中使用的变量,自动生成 setup 的 return 对象 ,省去手动 return 的冗余代码;
第三,将 import 导入的组件 自动注册 到 components 选项中;
第四,支持顶层 await,将其编译为 async setup 函数,配合 Suspense 实现异步渲染。
与普通 setup 相比,它有三个关键区别:一是代码量减少约 30%-50%;二是 TypeScript 类型推导更精准,无需手动标注 props 类型;三是默认封闭组件实例,必须通过
defineExpose显式暴露,安全性更好。在实际项目中,我推荐团队统一使用
<script setup>+ TypeScript + Volar 的组合,但在 SSR 水合、动态组件选项等极少数边界场景下,仍需回退到标准 setup 函数。理解其编译本质,能帮助我们在遇到宏报错、HMR 失效等问题时快速定位根因。"