一、概念层:编译产物的目标
经过前面六个阶段(宏解析 → 绑定分析 → 合并逻辑 → AST 遍历 → 模板编译 → 代码生成),
Vue 编译器最终需要生成一个标准的运行时组件定义:
javascript
export default defineComponent({
name: 'MyComp',
props: { title: String },
emits: ['submit'],
setup(__props, { expose, emit }) {
const msg = ref('hello')
expose()
return { msg }
}
})
这就是编译器的目标产物,它必须满足三个条件:
- 语义完整:包含所有声明的 props / emits / expose / setup 内容。
- 运行时可执行:直接可被浏览器或 bundler 执行。
- 调试可追踪 :保持 SourceMap 与原始
.vue文件一致。
二、原理层:defineComponent() 包裹生成逻辑
编译器在最后阶段会将 setup 内容包裹到 defineComponent() 调用中。
核心逻辑如下:
javascript
if (ctx.isTS) {
ctx.s.prependLeft(
startOffset,
`\n${genDefaultAs} /*@__PURE__*/${ctx.helper('defineComponent')}({${def}${runtimeOptions}\n ${hasAwait ? 'async ' : ''}setup(${args}) {\n${exposeCall}`
)
ctx.s.appendRight(endOffset, `})`)
} else {
ctx.s.prependLeft(
startOffset,
`\n${genDefaultAs} /*@__PURE__*/Object.assign(${defaultExport ? '__default__, ' : ''}{${runtimeOptions}\n ${hasAwait ? 'async ' : ''}setup(${args}) {\n${exposeCall}`
)
ctx.s.appendRight(endOffset, `})`)
}
这段代码做了三件关键事:
| 步骤 | 作用 |
|---|---|
| ① | 使用 prependLeft 在文件顶部插入 defineComponent({ |
| ② | 将 setup 函数体和 runtime 选项(props、emits)拼接 |
| ③ | 使用 appendRight 在文件末尾补全 }) |
三、对比层:TypeScript 与 JavaScript 输出差异
| 特征 | TypeScript 模式 | JavaScript 模式 |
|---|---|---|
| 导出方式 | defineComponent({...}) |
Object.assign(__default__, {...}) |
| 类型保留 | 保留 TS 泛型与类型推导 | 丢弃类型,仅保留结构 |
| 默认导出 | 使用 genDefaultAs(如 export default 或变量赋值) |
同上 |
| 合并策略 | 对象展开 ...__default__ |
使用 Object.assign() |
💡 TypeScript 模式允许类型系统理解
props与emits,JavaScript 模式则以运行时结构为主,更简洁。
四、实践层:运行时辅助选项注入
在生成组件定义对象时,Vue 会注入多个隐藏的运行时属性:
1️⃣ __name
lua
if (!ctx.hasDefaultExportName && filename && filename !== DEFAULT_FILENAME) {
const match = filename.match(/([^/\]+).\w+$/)
if (match) runtimeOptions += `\n __name: '${match[1]}',`
}
作用:
- 自动推导组件名(如
MyComp.vue→__name: 'MyComp'); - 用于开发工具(DevTools)和调试堆栈标识。
2️⃣ __isScriptSetup
php
if (!options.inlineTemplate && !__TEST__) {
ctx.s.appendRight(endOffset, `
const __returned__ = ${returned}
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
`)
}
作用:
- 标记此组件由
<script setup>编译而来; - 供运行时代理判断
_ctx可暴露哪些内部变量; - 在 Vue DevTools 中用于区分"组合式组件"。
3️⃣ __ssrInlineRender
r
if (hasInlinedSsrRenderFn) {
runtimeOptions += `\n __ssrInlineRender: true,`
}
作用:
- 在 SSR 模式下标记模板已被内联;
- 避免重复加载 render 函数;
- 优化服务端渲染的性能与缓存逻辑。
4️⃣ __expose()
ini
const exposeCall = ctx.hasDefineExposeCall || options.inlineTemplate ? '' : ' __expose();\n'
作用:
- 若用户未显式调用
defineExpose(),编译器自动插入默认调用; - 确保
<script setup>默认闭合暴露; - 避免无意间将局部变量泄漏到实例。
五、拓展层:返回值封装与响应式展开
在 setup 函数结束前,Vue 编译器会注入:
kotlin
return { ...setupBindings, ...scriptBindings }
如果检测到变量可能是 ref,则在模板层编译时自动展开 .value。
这一阶段结合了前文的 BindingTypes 结果,实现了"静态响应式展开"优化:
模板渲染时无需动态判定类型,而是直接生成正确的访问方式。
六、源文件映射与最终结构
生成的最终文件结构大致如下:
javascript
import { defineComponent as _defineComponent, ref as _ref } from "vue"
const __default__ = { name: "MyComp" }
export default /*#__PURE__*/_defineComponent({
...__default__,
__name: "MyComp",
props: { title: String },
emits: ["submit"],
async setup(__props, { expose, emit }) {
expose()
const msg = _ref('hello')
return { msg }
}
})
对应的 SourceMap
compileScript() 会通过前文的 mergeSourceMaps() 合并脚本与模板的映射:
- 所有行号、列号与原
.vue文件保持一致; - 开发者在浏览器或 VSCode 中打断点时,仍能跳回原始
.vue文件; - 即使模板被内联或 props 被重写,也不会影响定位。
七、潜在问题与设计思考
| 问题 | 说明 |
|---|---|
| 默认导出冲突 | 若用户在 <script> 与 <script setup> 中都使用默认导出,编译器会覆盖其中一个。 |
| 异步 setup | 若含有 await 表达式,编译器自动生成 async setup(),但需确保 SSR 环境兼容。 |
| 内联模板调试困难 | 内联模板虽然高效,但调试时行偏移需借助合并的 SourceMap。 |
| 宏与运行时 API 混用 | 宏(defineProps)与运行时函数(ref)作用域独立,混用会造成作用域提升错误。 |
八、小结
通过这一篇,我们理解了 Vue 编译器最终输出的组件结构:
defineComponent()的生成逻辑;- TypeScript / JS 两种模式的差异;
- 运行时特殊标识(
__name,__isScriptSetup,__ssrInlineRender); - setup 返回值与响应式展开机制;
- 最终代码结构与 SourceMap 一致性。
这一步是整个 compileScript() 流程的汇点 ------
将之前的语义分析、AST 操作、模板编译、响应式推断统一汇合为一个完整的运行时对象。
下一篇(第 8 篇)我们将进入本系列的最后部分:
🧩 《从编译原理视角重构 Vue SFC 编译器:宏系统与语义分析的未来》 ,
从编译器设计角度回顾 Vue SFC 的演化,并探讨宏系统在语言层的可扩展性。
本文部分内容借助 AI 辅助生成,并由作者整理审核。