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> 用于定义传统选项式属性(name、methods、mixins 等);
  • <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 辅助生成,并由作者整理审核。

相关推荐
奕鼎竜瑆1 小时前
Solid 前端响应式开发从零到精通
前端·人工智能
动恰客流统计1 小时前
景区客流统计怎么做?兼顾管控与运营的实施方案解析
大数据·前端·人工智能
派小心.2 小时前
A/B测试工具免费版够用吗?
前端·数据分析
前端snow3 小时前
ai agent --- redis 缓存
前端
duanshu_3 小时前
官网表单与下载异常怎么排查?五个环节定位故障
前端·自动化·flyweight
Zhou1411364 小时前
SpringBoot_03_Web开发
前端·spring boot·python
辻弋2014 小时前
【无标题】
大数据·服务器·前端·搜索引擎·开源软件
FluxArt4 小时前
GPT Image 2.5 中文提示词教程:Flux Art 可复用模板
前端·gpt·算法
搬砖的小码农_Sky5 小时前
AI Agent:Windows上Node.js安装后 npm -v报错
前端·npm·node.js·人机交互
计算机魔术师6 小时前
半年翻倍!OpenAI再融300亿,估值冲到1.4万亿
前端