Vue SFC 编译核心解析(第 4 篇)——普通 <script> 与 <script setup> 的合并逻辑

一、概念层:为什么要"合并"两个 <script>

Vue 3 的单文件组件(SFC)允许同时存在两种脚本块:

xml 复制代码
<script>
export default {
  name: 'HelloWorld',
  methods: { sayHi() { console.log('hi') } }
}
</script>

<script setup>
const msg = 'Hello Vue 3'
</script>

这两块在语义上是分开的:

  • 普通 <script> 用于定义传统选项式属性(namemethodsmixins 等);
  • <script setup> 则对应组合式 API 的 setup() 逻辑。

compileScript() 的任务就是将这两部分整合成一个完整的、合法的运行时代码:

javascript 复制代码
export default defineComponent({
  name: 'HelloWorld',
  setup() {
    const msg = 'Hello Vue 3'
    return { msg }
  }
})

二、原理层:整体合并思路

compileScript() 中这一逻辑的核心是 "默认导出重写" + "代码块移动" + "作用域合并"

换句话说,它不会简单地"拼接两个字符串",而是通过 AST 分析与 MagicString 操作完成精确重构。

整体算法思路如下:

xml 复制代码
if script exists:
    analyze <script> AST
    record default export or named exports
    move <script> content before <script setup>
    rewrite export default to const __default__
merge __default__ with setup() result
wrap defineComponent({...})

三、对比层:合并前后的结构变化

阶段 原始结构 编译后结构
输入 两个独立块:<script> + <script setup> 合并为一个组件定义
处理策略 分别解析 AST 通过 MagicString 操作整合
默认导出 export default {...} const __default__ = {...}
最终输出 各自独立 export default defineComponent({...})

💡 Vue 编译器通过将默认导出转换为变量,再与 setup() 一起合并,保证逻辑不冲突。


四、实践层:关键代码解析

1. 检测导出与重写逻辑

ini 复制代码
if (node.type === 'ExportDefaultDeclaration') {
  defaultExport = node
  // export default { ... } --> const __default__ = { ... }
  const start = node.start! + scriptStartOffset!
  const end = node.declaration.start! + scriptStartOffset!
  ctx.s.overwrite(start, end, `const ${normalScriptDefaultVar} = `)
}

逐行解释:

  • 检查 export default 节点;
  • MagicString.overwrite()export default 替换成 const __default__ =
  • normalScriptDefaultVar 是编译时定义的字符串 "__default__"
  • 这样做的结果是:导出的对象被保存为一个常量,稍后会和 setup() 代码组合。

2. 命名导出转换为导入或赋值

javascript 复制代码
if (node.type === 'ExportNamedDeclaration') {
  const defaultSpecifier = node.specifiers.find(
    s => s.exported.name === 'default',
  )
  if (defaultSpecifier) {
    defaultExport = node
    if (node.source) {
      ctx.s.prepend(
        `import { ${defaultSpecifier.local.name} as ${normalScriptDefaultVar} } from '${node.source.value}'\n`,
      )
    } else {
      ctx.s.appendLeft(
        scriptEndOffset!,
        `\nconst ${normalScriptDefaultVar} = ${defaultSpecifier.local.name}\n`,
      )
    }
  }
}

逐步说明:

  • 当发现命名导出语句如 export { foo as default }
  • 如果来自外部模块 → 转为 import { foo as __default__ } from '...'
  • 如果是本地变量 → 转为 const __default__ = foo
  • 确保后续 __default__ 始终存在可引用值。

3. 代码块位置调整(move)

由于 <script setup> 通常位于 <script> 之后,

编译器需要调整顺序,使普通脚本块先出现。

scss 复制代码
if (scriptStartOffset! > startOffset) {
  ctx.s.move(scriptStartOffset!, scriptEndOffset!, 0)
}

含义:

  • 如果 <script> 出现在 <script setup> 之后;
  • 使用 MagicString.move() 将它移动到文件最前;
  • 确保 __default__ 在 setup 函数之前被定义。

4. 最终合并点:运行时组件定义

到了最后阶段,compileScript() 会把两者合并为:

javascript 复制代码
ctx.s.prependLeft(
  startOffset,
  `
  export default defineComponent({
    ...__default__,
    ${runtimeOptions}
    setup(${args}) {
      ${exposeCall}
  `
)
ctx.s.appendRight(endOffset, `})`)

效果是:

  • __default__ 的内容展开放入 defineComponent()
  • 在同一个对象中注入 runtime props、emits、name 等;
  • <script setup> 内容变为 setup() 函数体。

五、拓展层:从语法到设计哲学

这种"拆分 + 合并"机制并不是 hack,而是一种编译层抽象设计:

特点 意义
非侵入式兼容 保留传统 <script> 的语义,不破坏旧代码。
编译期融合 在 AST 层统一逻辑,避免运行时合并开销。
统一输出模型 所有组件最终都转为 defineComponent() 格式。
易于扩展 后续宏(如 defineModel)可直接注入 setup 层。

Vue 团队的核心目标是:保持语法演进与运行时模型的一致性。


六、潜在问题与边界情况

情况 处理方式
<script><script setup> 使用不同语言(如 JS/TS) 抛出错误,强制语言一致。
两个块都导出默认对象 优先保留 <script> 的默认导出。
<script> 中手动定义 setup() 编译器不会注入第二个 setup(),需开发者自行合并逻辑。
作用域冲突(同名变量) 后者(<script setup>)的绑定会覆盖前者。

七、小结

在本篇中,我们剖析了:

  • 为什么 Vue 允许两个 <script> 共存;
  • compileScript() 如何重写 export default
  • 如何利用 MagicString 调整代码顺序;
  • 最终如何合并成标准的 defineComponent() 输出。

通过这一机制,Vue 实现了传统选项式与组合式语法的无缝过渡。


在下一篇(第 5 篇),我们将深入底层实现细节:

🧩 《AST 遍历与声明解析:walkDeclaration 系列函数详解》

讲解编译器是如何递归遍历变量声明模式、识别 const/let、推断响应式特征的。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

相关推荐
excel1 小时前
Vue SFC 编译核心解析(第 3 篇)——绑定分析与作用域推断
前端
excel1 小时前
Vue SFC 编译核心解析(第 1 篇)——compileScript 总体流程概览
前端
excel1 小时前
Vue 编译器中的 processAwait 实现深度解析
前端
excel2 小时前
Vue SFC 编译核心解析(第 2 篇)——宏函数解析机制
前端
excel2 小时前
🔍 Vue 模板编译中的资源路径转换机制:transformAssetUrl 深度解析
前端
excel2 小时前
Vue 模板编译中的 srcset 机制详解:从 HTML 语义到编译器实现
前端
excel2 小时前
🌐 从 Map 到 LRUCache:构建智能缓存工厂函数
前端
excel2 小时前
Vue 模板编译中的资源路径转换:transformSrcset 深度解析
前端
excel2 小时前
Vue 工具函数源码解析:URL 解析与分类逻辑详解
前端