<script setup> 是 Vue 3 引入的一项革命性语法糖,它极大地简化了组合式 API 的书写体验:无需手动 return 暴露变量,自动处理组件注册,代码更加简洁。这主要依赖于 Vue 编译器(@vue/compiler-sfc)的复杂工作。
从 Babel 解析到最终生成可执行的组件 JavaScript 代码,从compileScript 函数来深入了解 <script setup> 块的编译过程,
compileScript
compileScript 是 Vue 3 编译器(@vue/compiler-sfc)中用于处理单文件组件(SFC)<script> 和 <script setup> 块的核心函数。它的职责是将 SFC 中的脚本部分(普通 <script> 和/或 <script setup>)编译成一个最终的、可运行的组件对象。
一、解析
编译的第一步,是使用 Babel 的 parse 函数,将 <script setup> 标签内的原始代码字符串,转换为一个结构化的 JavaScript 抽象语法树(AST)
二、导入收集与去重
遍历两个块的 ImportDeclaration,将导入信息注册到 ctx.userImports;对重复或冲突的导入进行去重或报错;将宏导入(如 defineProps)从代码中移除。
registerUserImport
将导入节点处理存储在 ctx.userImports
三、普通 <script> 处理
- 提取
export default中的组件选项(name、render等),并重写为const __default__ = { ... }。 - 处理命名导出,将
export { x as default }转换为const __default__ = x。 - 遍历变量/函数声明,记录绑定类型到
scriptBindings。
四、<script setup> 主体处理
- 识别并移除/转换编译器宏(
defineProps、defineEmits、defineExpose、defineOptions、defineSlots、defineModel),生成对应的运行时代码(如__props、__emit、__expose等)。 - 遍历变量声明,记录绑定类型到
setupBindings,并对纯静态常量进行提升(hoistNode)。 - 检测顶层
await,标记hasAwait并注入__temp、__restore变量,使用_withAsyncContext处理异步上下文。 - 检查 ES 模块导出(不允许),并将 TypeScript 类型声明提升到模块顶部。
五、Props 解构转换
若使用了 defineProps 解构,调用 transformDestructuredProps 生成代理代码(createPropsRestProxy)。
六、绑定元数据分析
合并 scriptBindings、setupBindings 和 userImports,生成 bindingMetadata(供模板编译使用,如自动 .value 解包)。
七、CSS 变量注入
若存在 v-bind CSS 变量,注入 useCssVars 调用。
八、生成 setup 函数主体
- 构建
setup函数的参数签名(__props,以及可选的{ expose: __expose, emit: __emit })。 - 生成
return语句:在非内联模板模式下,返回一个包含所有模板所需绑定的对象(对import绑定生成 getter,对let绑定生成 getter/setter);在内联模板模式下,直接内联模板编译后的渲染函数。 - 若存在顶层
await,函数标记为async。
九、组装最终组件定义
- 使用
defineComponent(TS 模式)或Object.assign(JS 模式)包裹,合并普通<script>的默认导出、defineOptions结果以及运行时生成的setup函数。 - 添加
__name(从文件名推断)、props、emits等运行时选项。 - 注入辅助函数。
十、输出与 Source Map
使用 magic-string 完成所有插入、删除、移动操作,生成最终代码;同时生成 Source Map,并可选地与模板内联编译的 map 合并。
注意事项
- 一个文件组件不能同时存在两个script 或者两个 script setup。
- script 标签不能有 src 属性
3、script 和 script setup块的语言要一致。
4、导入处理。宏不需要手动导入,也不可起别名。
5、导出处理。script setup 本身已经有一个隐式的默认导出,不能再写一个默认导出。类型导出可以支持。
[@vue/compiler-sfc] <script setup> cannot contain ES module exports.
If you are using a previous version of <script setup>,
please consult the updated RFC at https://github.com/vuejs/rfcs/pull/227.
示例 导入本地图片
<template>
<div>
<img :src="logo" alt="" />
</div>
</template>
<script setup lang="ts">
import logo from "@/assets/logo.svg";
defineOptions({
name: "CloudIndexView",
});
</script>
import { defineComponent as _defineComponent } from "vue";
import logo from "@/assets/logo.svg";
const _sfc_main = /*@__PURE__*/ _defineComponent({
...{
name: "CloudIndexView",
},
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
const __returned__ = {
get logo() {
return logo;
},
};
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
});
示例 各种声明变量
<template>
<div>
<p>这里是云平台首页</p>
<tabOne></tabOne>
</div>
</template>
<script setup lang="ts">
import tabOne from "@/pages/cloud/components/tabOne.vue";
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
const tabName = ref("tab");
const age = 20;
let desc = "z在18路";
const route = useRoute();
onMounted(() => {
console.log("before", tabName.value, route, age, desc);
desc = "z在19路";
console.log("after", tabName.value, route, age, desc);
});
defineOptions({
name: "CloudIndexView",
});
</script>
import { defineComponent as _defineComponent } from "vue";
import tabOne from "@/pages/cloud/components/tabOne.vue";
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
const age = 20;
const _sfc_main = /*@__PURE__*/ _defineComponent({
...{
name: "CloudIndexView",
},
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
const tabName = ref("tab");
let desc = "z在18路";
const route = useRoute();
onMounted(() => {
console.log("before", tabName.value, route, age, desc);
desc = "z在19路";
console.log("after", tabName.value, route, age, desc);
});
const __returned__ = {
tabName,
age,
get desc() {
return desc;
},
set desc(v) {
desc = v;
},
route,
tabOne,
};
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
});
示例 插槽 useSlots
子组件
<template>
<div>
<p>这里是 tabTwo 标题</p>
<template v-for="(_, name) in slots" :key="name">
<slot :name="name" :data="info"></slot>
</template>
</div>
</template>
<script setup lang="ts">
import { useSlots } from "vue";
defineProps<{
info: {
buttonName: string;
};
}>();
const slots = useSlots();
</script>
import { useSlots } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
const _sfc_main = /* @__PURE__ */ _defineComponent({
__name: "tabTwo",
props: { info: {
type: Object,
required: true
} },
setup(__props, { expose: __expose }) {
__expose();
const slots = useSlots();
const __returned__ = { slots };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true
});
return __returned__;
}
});
import { createElementVNode as _createElementVNode, renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, renderSlot as _renderSlot } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, [_cache[0] || (_cache[0] = _createElementVNode(
"p",
null,
"这里是 tabTwo 标题",
-1
/* CACHED */
)), (_openBlock(true), _createElementBlock(
_Fragment,
null,
_renderList($setup.slots, (_, name) => {
return _renderSlot(_ctx.$slots, name, {
key: name,
data: $props.info
});
}),
128
/* KEYED_FRAGMENT */
))]);
}
父组件
<template>
<p>{{ "这里是云平台首页" }}</p>
<tabTwo :info="info">
<template #default>
<p>这里是 插槽 default 部分</p>
</template>
<template #body>
<p>这里是 插槽 body 部分</p>
</template>
<template #footer="{ data }">
<button>{{ data.buttonName }}</button>
</template>
</tabTwo>
</template>
<script setup lang="ts">
import tabTwo from "@/pages/cloud/components/tabTwo.vue";
import { reactive } from "vue";
const info = reactive({
buttonName: "提交",
});
defineOptions({
name: "CloudIndexView",
});
defineSlots<{
default(): void;
body(): void;
footer(data: { buttonName: string }): void;
}>();
</script>
import tabTwo from "/src/pages/cloud/components/tabTwo.vue?t=1776928310813";
import { reactive } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
const _sfc_main = /* @__PURE__ */ _defineComponent({
...{ name: "CloudIndexView" },
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
const info = reactive({ buttonName: "提交" });
const __returned__ = {
info,
tabTwo
};
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true
});
return __returned__;
}
});
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, withCtx as _withCtx, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
_Fragment,
null,
[_cache[2] || (_cache[2] = _createElementVNode(
"p",
null,
_toDisplayString("这里是云平台首页"),
-1
/* CACHED */
)), _createVNode($setup["tabTwo"], { info: $setup.info }, {
default: _withCtx(() => [..._cache[0] || (_cache[0] = [_createElementVNode(
"p",
null,
"这里是 插槽 default 部分",
-1
/* CACHED */
)])]),
body: _withCtx(() => [..._cache[1] || (_cache[1] = [_createElementVNode(
"p",
null,
"这里是 插槽 body 部分",
-1
/* CACHED */
)])]),
footer: _withCtx(({ data }) => [_createElementVNode(
"button",
null,
_toDisplayString(data.buttonName),
1
/* TEXT */
)]),
_: 1
}, 8, ["info"])],
64
/* STABLE_FRAGMENT */
);
}
示例 使用顶层await
<template>
<div>
<p>这里是tabOne 。。。。{{ tabName }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const tabName = ref("本tabOne");
const promise: { message: string } = await new Promise((resolve) => {
resolve({ message: "测试await" });
});
tabName.value = promise.message;
console.log(tabName.value);
defineOptions({
name: "TabOneView",
});
</script>
import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/pages/cloud/components/tabOne.vue");import { withAsyncContext as _withAsyncContext, defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
import { ref } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
const _sfc_main = /* @__PURE__ */ _defineComponent({
...{ name: "TabOneView" },
__name: "tabOne",
async setup(__props, { expose: __expose }) {
__expose();
let __temp, __restore;
const tabName = ref("本tabOne");
const promise = ([__temp, __restore] = _withAsyncContext(async () => new Promise((resolve) => {
resolve({ message: "测试await" });
})), __temp = await __temp, __restore(), __temp);
tabName.value = promise.message;
console.log(tabName.value);
const __returned__ = {
tabName,
promise
};
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true
});
return __returned__;
}
});
示例 导出 interface
【示例】
<template>
<div>
<p>这里是云平台首页</p>
</div>
</template>
<script setup lang="ts">
export interface Props {
tabName: string;
age: number;
}
defineOptions({
name: "CloudIndexView",
});
</script>
import { defineComponent as _defineComponent } from "vue";
export interface Props {
tabName: string;
age: number;
}
const _sfc_main = /*@__PURE__*/ _defineComponent({
...{
name: "CloudIndexView",
},
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
const __returned__ = {};
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
__returned__;
},
});
示例 导出 type
<template>
<div>
<p>这里是云平台首页</p>
</div>
</template>
<script setup lang="ts">
export type Props = {
tabName: string;
age: number;
};
defineOptions({
name: "CloudIndexView",
});
</script>
import { defineComponent as _defineComponent } from "vue";
export type Props = {
tabName: string;
age: number;
};
const _sfc_main = /*@__PURE__*/ _defineComponent({
...{
name: "CloudIndexView",
},
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
const __returned__ = {};
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
});
示例 导出 declare
<template>
<div>
<p>这里是云平台首页</p>
</div>
</template>
<script setup lang="ts">
export declare const API_URL: string;
defineOptions({
name: "CloudIndexView",
});
</script>
import { defineComponent as _defineComponent } from "vue";
export declare const API_URL: string;
const _sfc_main = /*@__PURE__*/ _defineComponent({
...{
name: "CloudIndexView",
},
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
const __returned__ = {};
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
});
示例 使用枚举
<template>
<div>
<p>这里是云平台首页</p>
</div>
</template>
<script setup lang="ts">
enum UserRole {
Admin = "admin",
Editor = "editor",
Viewer = "viewer",
}
console.log(UserRole.Admin);
defineOptions({
name: "CloudIndexView",
});
</script>
编译结果
import { defineComponent as _defineComponent } from "vue";
enum UserRole {
Admin = "admin",
Editor = "editor",
Viewer = "viewer",
}
const _sfc_main = /*@__PURE__*/ _defineComponent({
...{
name: "CloudIndexView",
},
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
console.log(UserRole.Admin);
const __returned__ = { UserRole };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
});
import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/pages/cloud/index.vue");import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
var UserRole = /* @__PURE__ */ function(UserRole) {
UserRole["Admin"] = "admin";
UserRole["Editor"] = "editor";
UserRole["Viewer"] = "viewer";
return UserRole;
}(UserRole || {});
const _sfc_main = /* @__PURE__ */ _defineComponent({
...{ name: "CloudIndexView" },
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
console.log(UserRole.Admin);
const __returned__ = { UserRole };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true
});
return __returned__;
}
});
示例 style 注入变量
<template>
<div>
<p>这里是云平台首页</p>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const color = ref("red");
defineOptions({
name: "CloudIndexView",
});
</script>
<style lang="less" scoped>
p {
color: v-bind(color);
}
</style>
import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/pages/cloud/index.vue");import { useCssVars as _useCssVars, defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
import { ref } from "/node_modules/.vite/deps/vue.js?v=2d0c80e8";
const _sfc_main = /* @__PURE__ */ _defineComponent({
...{ name: "CloudIndexView" },
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
_useCssVars((_ctx) => ({ "60451c3f-color": color.value }));
const color = ref("red");
const __returned__ = { color };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true
});
return __returned__;
}
});
枚举 BindingTypes
enum BindingTypes {
/**
* returned from data()
* 从 data() 函数返回的变量
*/
DATA = 'data',
/**
* declared as a prop
* 作为组件的属性声明的变量
*/
PROPS = 'props',
/**
* a local alias of a `<script setup>` destructured prop.
* the original is stored in __propsAliases of the bindingMetadata object.
* script setup 函数中解构赋值的属性别名
*/
PROPS_ALIASED = 'props-aliased',
/**
* a let binding (may or may not be a ref)
* 使用 let 声明的绑定
*/
SETUP_LET = 'setup-let',
/**
* a const binding that can never be a ref.
* these bindings don't need `unref()` calls when processed in inlined
* template expressions.
* 永远不会是 ref 的 const 绑定
*/
SETUP_CONST = 'setup-const',
/**
* a const binding that does not need `unref()`, but may be mutated.
* 不需要 unref() 但可能被修改的 const 绑定
*/
SETUP_REACTIVE_CONST = 'setup-reactive-const',
/**
* a const binding that may be a ref.
* 可能是 ref 的 const 绑定
*/
SETUP_MAYBE_REF = 'setup-maybe-ref',
/**
* bindings that are guaranteed to be refs
* 保证是 ref 的绑定
*/
SETUP_REF = 'setup-ref',
/**
* declared by other options, e.g. computed, inject
* 通过其他选项声明的绑定
*/
OPTIONS = 'options',
/**
* a literal constant, e.g. 'foo', 1, true
* 字面量常量
*/
LITERAL_CONST = 'literal-const',
}