查看: 232|回复: 3

Vue3 script setup编译语法糖与setup()区别

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
不少讲 Vue 3 的资料把 <script setup> 说成一种新的响应式 API,这是误解。它既不是运行时特性,也不是新的组件写法,而是 Vue 3 单文件组件(SFC)中组合式 API 的编译时语法糖,由 @vue/compiler-sfc 在构建阶段把它转换成标准的 setup() 函数。浏览器最终执行的代码与手写 setup() 完全一致,运行时没有额外开销,它的价值在于把样板代码的复杂度转移到了编译阶段。

一、编译期完成的四步转换

1. 宏提取:编译器识别 defineProps、defineEmits 等宏调用,把参数转成组件的 props / emits 选项对象,并在运行时代码中删除这些函数调用本身。

2. 变量收集:通过 AST 静态分析 <template> 中引用了哪些标识符,自动把这些顶层绑定收集进 setup 的返回对象(编译中间态会有一份 __returned__ 绑定集合),从而省掉手写 return。

3. 组件注册:扫描 import 语句,判断导入的是否为组件,若是则自动加入 components 选项。

4. 代码转换:把用户写的顶层逻辑整体包裹进 setup() 函数体,注入自动生成的返回对象;遇到顶层 await 时把 setup 标记为 async,配合 Suspense 使用。

二、源码与编译产物的对照

先看一段使用 <script setup> 的源码:
  1. <script setup lang="ts">
  2. import MyButton from './MyButton.vue'
  3. const props = defineProps<{ msg: string }>()
  4. const emit = defineEmits<{ change: [val: string] }>()
  5. const data = await fetchData()
  6. const handleClick = () => emit('change', 'clicked')
  7. defineExpose({ handleClick })
  8. </script>
  9. <template>
  10.   <MyButton @click="handleClick">{{ msg }}</MyButton>
  11. </template>
复制代码

经过 @vue/compiler-sfc 处理后,等价产物大致如下(简化示意):
  1. import MyButton from './MyButton.vue'
  2. import { resolveComponent as _resolveComponent } from 'vue'
  3. export default {
  4.   props: { msg: { type: String, required: true } },
  5.   emits: ['change'],
  6.   components: { MyButton },
  7.   async setup(__props, { expose, emit }) {
  8.     const data = await fetchData()
  9.     const handleClick = () => emit('change', 'clicked')
  10.     expose({ handleClick })
  11.     return {
  12.       msg: __props.msg,
  13.       MyButton,
  14.       handleClick,
  15.       data
  16.     }
  17.   }
  18. }
复制代码

可以看到,宏被翻译成了选项对象,import 被翻译成了 components,顶层变量被翻译成了 return 的键,defineExpose 被翻译成 expose 调用。整段代码在运行时与手写 setup 并无区别。

三、与普通 setup() 的逐项差异

执行阶段:普通 setup() 在组件实例创建后、渲染前执行,处在 beforeCreate 到 created 之间;<script setup> 的代码经编译后同样作为 setup() 的内容,在同一阶段执行。

代码结构:普通 setup 需要写成 export default { setup(props, ctx) { ... return {...} } };<script setup> 不需要 export default,顶层就是逻辑主体。

模板暴露:普通 setup 必须显式 return 模板用到的变量;<script setup> 的顶层声明默认自动暴露给模板,defineProps 等宏除外。

子组件注册:普通 setup 要在 components 选项里手动登记;<script setup> 中 import 即自动注册。

TypeScript 支持:普通 setup 需要额外写类型注解或泛型;<script setup lang="ts"> 原生支持,props 与 emits 的类型推导更准确。

性能表现:普通 setup 的模板依赖 <script> 上下文代理;<script setup> 的模板编译为同作用域渲染函数,减少代理开销,产物也更利于 Tree-shaking。

混合使用:<script setup> 可以与普通 <script> 共存,普通块用来声明 inheritAttrs、模块级单例等,setup 块写实例级逻辑。

四、一个高频误区:执行次数

网上常有“普通 <script> 只执行一次、<script setup> 每个实例都执行”的说法,这是把模块作用域和 setup 作用域混淆了。准确的说法是:两者在组件实例化时同步执行一次,执行时机完全一致。真正只执行一次的是普通 <script> 的顶层代码,因为它位于模块作用域,只在模块加载时运行;而 <script setup> 内的代码属于 setup 作用域,随每个组件实例创建而执行。

五、编译器宏不是普通函数

defineProps、defineEmits、defineExpose、defineOptions、defineSlots 都是编译器宏,不是真实的 JavaScript 函数,运行时并不存在。因此不能对它们解构、重命名或动态传参,只有 TypeScript 泛型写法是允许的。

另一条容易忽略的规则是实例封闭性:<script setup> 中定义的变量默认对模板公开,但父组件通过 ref 拿到子组件实例后并不能直接访问,必须由 defineExpose 显式暴露。这一点与 Options API 不同,属于有意的封装设计。

六、边界场景

<script setup> 只能用在 .vue 单文件组件中,在 .ts / .js 文件里无法使用,只能写标准 setup() 函数。

与普通 <script> 共存时,普通块只适合做三件事:声明全局类型、执行副作用、导出非组件内容;name、inheritAttrs 这类组件选项在 Vue 3.3+ 中需要用 defineOptions 宏声明。

由于编译器能精确追踪依赖变化,<script setup> 对 HMR 更友好;同时编译产物更精确,未使用的导入与变量更容易被剔除,打包体积通常小于手写的 setup。

七、工程实践建议

新项目默认使用 <script setup> + TypeScript + Volar(vue-official 插件)的组合,这是 Vue 官方文档的首选方案,也能获得完整的宏类型提示。只有在 SSR 水合、动态组件选项等少数需要回退到标准 setup 函数的场景,才考虑混写。理解它的编译本质,在遇到宏报错、HMR 失效、父组件拿不到子组件方法时,就能快速定位到是编译阶段还是暴露策略的问题,而不是把它当成运行时 bug 去排查。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Vue3 script setup编译语法糖与setup()区别

这篇总结很清楚,把 script setup 定位成 @vue/compiler-sfc 的编译时语法糖,而不是新的响应式 API,这点很关键。四步转换和源码到编译产物的对照很直观,尤其是宏提取、自动 return、组件自动注册和顶层 await 处理。执行次数那个误区解释得也到位,普通 script 顶层属于模块作用域,script setup 属于 setup 作用域,两者不能混为一谈。编译器宏不能当普通函数用、实例需要通过 defineExpose 显式暴露这两点也提醒得及时。结尾 defineExpose 那段好像被截断了,如果方便可以补全一下。感谢分享,对理解编译期和运行时的边界很有帮助。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Vue3 script setup编译语法糖与setup()区别

这篇梳理得很到位,尤其是把“script setup 不是新的响应式 API”和“执行次数”那个误区单独拎出来讲,确实容易混。我自己从 setup() 迁过来最直观的感受就是模板暴露和子组件注册省了很多手工活,但宏不能解构、不能重命名这条也踩过坑,习惯性写 const { foo } = defineProps 直接编译报错,后来才反应过来它压根不是运行时函数。 另外混合使用普通 script 声明 inheritAttrs、组件 name 这些也挺实用,相当于模块级和实例级逻辑分开写。想请教一下顶层 await 配 Suspense 的场景,异步数据加载失败一般怎么处理比较顺手?是在 setup 里 try/catch 兜住,还是交给上层 onErrorCaptured 统一处理?
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Vue3 script setup编译语法糖与setup()区别

楼主这篇梳理得很到位,尤其是把 script setup 定位成编译时语法糖而不是新的响应式 API,这个点确实容易被一些入门教程带偏。我平时看编译产物那一节最有共鸣,props/emits 变成选项对象、import 变成 components、顶层绑定变成 return 的键,基本上一眼就能对上“其实还是 setup()”这个结论。 补充几个实际开发里的体会: 一是宏不能解构、重命名这条规则,踩过坑的人应该都有印象。比如把 defineProps 的返回值直接解构,虽然 Vue 3.5 之后 props 解构在响应性上有改善,但宏调用本身仍然受编译器静态分析的约束,不能当成普通函数去动态传参或者包一层再调用,这点帖子最后说得比较清楚。 二是 defineExpose 的实例封闭性确实容易被忽略。很多人习惯了 Options API 里父组件 ref 拿到子组件实例就能访问方法和数据,切到 script setup 后默认什么都不暴露,往往要调试一阵才想起来补 defineExpose。这个设计从封装角度是合理的,但需要提前有个预期。 三是顶层 await 配合 Suspense 那个点,编译成 async setup 之后,组件的加载时序和错误处理都会跟普通 setup 不太一样,实际用的时候最好配合 fallback 和 onErrorCaptured 一起考虑,不然容易出现
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-31 07:52 , Processed in 0.028678 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部