vue3.x 编译 script setup 编译过程

2 阅读7分钟

<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>)编译成一个最终的、可运行的组件对象。

image.png

一、解析

编译的第一步,是使用 Babel 的 parse 函数,将 <script setup> 标签内的原始代码字符串,转换为一个结构化的 JavaScript 抽象语法树(AST)

image.png

image.png

二、导入收集与去重

遍历两个块的 ImportDeclaration,将导入信息注册到 ctx.userImports;对重复或冲突的导入进行去重或报错;将宏导入(如 defineProps)从代码中移除。

image.png

registerUserImport

将导入节点处理存储在 ctx.userImports

image.png

三、普通 <script> 处理

  1. 提取 export default 中的组件选项(namerender 等),并重写为 const __default__ = { ... }
  2. 处理命名导出,将 export { x as default } 转换为 const __default__ = x
  3. 遍历变量/函数声明,记录绑定类型到 scriptBindings

四、<script setup> 主体处理

  1. 识别并移除/转换编译器宏(definePropsdefineEmitsdefineExposedefineOptionsdefineSlotsdefineModel),生成对应的运行时代码(如 __props__emit__expose 等)。
  2. 遍历变量声明,记录绑定类型到 setupBindings,并对纯静态常量进行提升(hoistNode)。
  3. 检测顶层 await,标记 hasAwait 并注入 __temp__restore 变量,使用 _withAsyncContext 处理异步上下文。
  4. 检查 ES 模块导出(不允许),并将 TypeScript 类型声明提升到模块顶部。

image.png

五、Props 解构转换

若使用了 defineProps 解构,调用 transformDestructuredProps 生成代理代码(createPropsRestProxy)。

六、绑定元数据分析

合并 scriptBindingssetupBindings 和 userImports,生成 bindingMetadata(供模板编译使用,如自动 .value 解包)。

七、CSS 变量注入

若存在 v-bind CSS 变量,注入 useCssVars 调用。

八、生成 setup 函数主体

  1. 构建 setup 函数的参数签名(__props,以及可选的 { expose: __expose, emit: __emit })。
  2. 生成 return 语句:在非内联模板模式下,返回一个包含所有模板所需绑定的对象(对 import 绑定生成 getter,对 let 绑定生成 getter/setter);在内联模板模式下,直接内联模板编译后的渲染函数。
  3. 若存在顶层 await,函数标记为 async

九、组装最终组件定义

  1. 使用 defineComponent(TS 模式)或 Object.assign(JS 模式)包裹,合并普通 <script> 的默认导出、defineOptions 结果以及运行时生成的 setup 函数。
  2. 添加 __name(从文件名推断)、propsemits 等运行时选项。
  3. 注入辅助函数。

十、输出与 Source Map

使用 magic-string 完成所有插入、删除、移动操作,生成最终代码;同时生成 Source Map,并可选地与模板内联编译的 map 合并。

image.png

注意事项

  1. 一个文件组件不能同时存在两个script 或者两个 script setup。

image.png

image.png

  1. script 标签不能有 src 属性

image.png

3、script 和 script setup块的语言要一致。

image.png

4、导入处理。宏不需要手动导入,也不可起别名。

image.png

image.png

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.

image.png

示例 导入本地图片

<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__;
  },
});

image.png

image.png

示例 插槽 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>

image.png

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>

image.png

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__;
  },
});

image.png

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',
}

最后

  1. 在线查看AST