普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月9日技术

Vue SSR 组件转换源码深度解析:ssrTransformComponent.ts

作者 excel
2025年11月9日 07:31

一、概念:Vue SSR 转换的目的

在 Vue 3 的编译体系中,模板编译会分为 普通渲染(client render)服务器端渲染(SSR render) 两种模式。
客户端模式下生成的代码依赖 h()createVNode() 等运行时函数,而 SSR 模式下的编译产物则需要能直接输出字符串片段,用于服务端生成 HTML。

ssrTransformComponent 模块正是用于在 编译阶段 处理 <Component /> 标签在 SSR 模式下的代码生成逻辑。
它的目标是让 Vue 组件在服务端编译时能生成对应的 _push(...) 片段,以供后续的 ssrRenderComponent() 调用。


二、原理:两阶段 SSR 转换机制

源码的注释开头明确指出:

// ssr component transform is done in two phases:

Vue 的 SSR 编译对组件分两步处理:

  1. 第一阶段(transform phase)

    • 利用 buildSlots 分析组件的插槽结构;
    • 创建未完成的插槽函数(WIPSlotEntry),并保存在 WeakMap 中;
    • 为组件生成 SSR Codegen Node 占位符。
  2. 第二阶段(process phase)

    • 遍历第一阶段记录的 WIPSlotEntry
    • 填充每个 slot 的函数体(即 _push 输出或 VNode 分支);
    • 输出最终可执行的 _ssrRenderComponent(...)_ssrRenderVNode(...) 调用。

这两阶段分别对应源码中的:

  • ssrTransformComponent(转换器)
  • ssrProcessComponent(处理器)

三、对比:SSR 与 Client 渲染差异

项目 Client 渲染 SSR 渲染
渲染目标 VNode 树 HTML 字符串
调用接口 createVNode() / renderComponentRoot() _push() / ssrRenderComponent()
插槽处理 renderSlot() _ssrRenderSlot()
动态组件 resolveDynamicComponent() ssrRenderVNode()
转换阶段 单次编译 两阶段转换(分析 + 生成)

由此可见,SSR 编译需要在编译期模拟组件行为,以提前生成完整的 HTML 输出逻辑。


四、实践:源码逐段讲解

4.1 数据结构与全局变量

const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
const componentTypeMap = new WeakMap<ComponentNode, string | symbol | CallExpression>()
  • wipMap:存储当前组件未完成的插槽函数信息;
  • componentTypeMap:记录组件对应的类型(静态字符串、动态调用、内置组件符号)。

WIPSlotEntry 的定义如下:

interface WIPSlotEntry {
  type: typeof WIP_SLOT
  fn: FunctionExpression
  children: TemplateChildNode[]
  vnodeBranch: ReturnStatement
}

表示一个“正在构建中的”插槽函数,等待第二阶段补全。


4.2 SSR 转换主函数

export const ssrTransformComponent: NodeTransform = (node, context) => {
  if (node.type !== NodeTypes.ELEMENT || node.tagType !== ElementTypes.COMPONENT) return
  • 仅在节点为“组件类型元素”时触发。
  • 通过 resolveComponentType() 判断组件类型(静态、动态或内置组件)。
const component = resolveComponentType(node, context, true /* ssr */)

此时若检测到内置组件(如 <Suspense><Teleport><Transition>),会直接交由对应的专用转换函数处理。


4.3 构建 vnode fallback 分支

SSR 渲染中,部分插槽可能退回到普通 VNode 渲染。
因此,这里克隆当前节点,通过 buildSlots() 构建备用的 VNode 渲染分支:

const vnodeBranches: ReturnStatement[] = []
const clonedNode = clone(node)

buildSlots 会返回多个 slot,每个 slot 对应一个 ReturnStatement,记录备用渲染逻辑。


4.4 构建 SSR 属性与插槽函数

const { props, directives } = buildProps(node, context, undefined, true, isDynamicComponent)
const propsExp = buildSSRProps(props, directives, context)
  • SSR 模式下的属性生成需过滤掉无关事件;
  • buildSSRProps() 会转换属性为字符串拼接形式。

接着定义一个 SlotFnBuilder

const buildSSRSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => {
  const fn = createFunctionExpression([...], undefined, true, true, loc)
  wipEntries.push({ fn, children, vnodeBranch })
  return fn
}

这一步只是构造函数签名(参数为 _push, _parent, _scopeId 等),函数体在第二阶段填充。


4.5 生成最终 SSR 调用节点

静态组件使用:

node.ssrCodegenNode = createCallExpression(
  context.helper(SSR_RENDER_COMPONENT),
  [component, propsExp, slots, `_parent`]
)

动态组件(通过 resolveDynamicComponent)使用:

node.ssrCodegenNode = createCallExpression(
  context.helper(SSR_RENDER_VNODE),
  [
    `_push`,
    createCallExpression(context.helper(CREATE_VNODE), [component, propsExp, slots]),
    `_parent`
  ]
)

区别在于 SSR 是否需要额外创建 VNode 实例来决定渲染逻辑。


4.6 第二阶段:ssrProcessComponent

此函数在第二轮遍历中被调用,用于真正生成可执行代码。

填充 Slot 函数体

fn.body = createIfStatement(
  createSimpleExpression(`_push`, false),
  processChildrenAsStatement(wipEntries[i], context, false, true),
  vnodeBranch
)

即生成如下伪代码:

if (_push) {
  // SSR 输出逻辑
} else {
  // VNode fallback 逻辑
}

最终输出

若组件为静态:

context.pushStatement(createCallExpression(`_push`, [node.ssrCodegenNode]))

若动态组件:

context.pushStatement(node.ssrCodegenNode)

4.7 子转换与辅助函数

createVNodeSlotBranch()subTransform() 实现了子上下文的 VNode 模式编译,以保证插槽作用域正确继承。

clone() 则为深度克隆 AST 节点的递归实现:

function clone(v: any): any {
  if (isArray(v)) return v.map(clone)
  else if (isPlainObject(v)) { ... }
  else return v
}

五、拓展:可结合 SSR 插槽的运行机制理解

  • ssrRenderSlot() 在运行时调用这些生成的函数;
  • _push() 代表向最终 HTML 字符串流写入;
  • SSR 插槽的作用域与客户端一致,但行为不同:SSR 中是同步展开,Client 中是延迟渲染。

六、潜在问题与优化方向

  1. WeakMap 引用的生命周期问题
    若编译器复用上下文过久,wipMap 未清理可能导致内存占用上升。
  2. vnodeFallback 的冗余生成
    在多数情况下,SSR 插槽不会回退至 VNode 模式,可优化以按需生成。
  3. Dynamic Component 的边界场景
    SSR 渲染时,若动态组件解析失败(如外部依赖未加载),ssrRenderVNode 可能输出空标签。

七、总结

本文分析了 Vue SSR 组件转换源码的核心逻辑,完整展示了:

  • SSR 编译的双阶段设计;
  • 插槽函数的延迟补全机制;
  • 静态与动态组件的差异处理;
  • 子上下文继承与 VNode fallback 机制。

这份源码展示了 Vue 3 编译体系在“声明式模板 → 渲染指令代码”间的严谨抽象,是理解 Vue SSR 的核心部分。


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

Vue SSR 编译器核心逻辑解析:ssrInjectFallthroughAttrs

作者 excel
2025年11月9日 07:26

一、概念:什么是 “Fallthrough Attributes”

在 Vue 组件体系中,Fallthrough Attributes(透传属性)是指那些组件未显式声明 props,但仍应透传到内部根节点的 HTML 属性或绑定。例如:

<MyButton class="primary" id="ok" />

如果 MyButton 没有 classid 的 prop,这些属性就会透传到内部元素 <button> 上。
SSR(服务端渲染) 场景下,Vue 编译器需要确保这些属性能正确注入到最终生成的 HTML 字符串中。

ssrInjectFallthroughAttrs 这个 NodeTransform 就是在编译阶段自动注入 _attrs 的关键逻辑。


二、原理:AST 转换与属性注入机制

1. 入口结构

export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => { ... }

Vue 的模板编译过程会构建一棵 AST(抽象语法树) ,然后通过一系列 NodeTransform 对节点进行分析与改写。

  • node:当前遍历的 AST 节点。
  • context:编译上下文,包含父节点、root 引用、已注册的标识符等信息。

2. 关键判断:ROOT 层初始化

if (node.type === NodeTypes.ROOT) {
  context.identifiers._attrs = 1
}

意义:在 SSR 模式中 _attrs 作为函数参数传入,这里手动标记它为 “已声明变量”,以防编译器错误地对其添加前缀(如 _ctx._attrs)。


三、对比:处理不同类型组件的场景差异

1. Transition / KeepAlive 特殊情况

if (
  node.type === NodeTypes.ELEMENT &&
  node.tagType === ElementTypes.COMPONENT &&
  (node.tag === 'transition' ||
    node.tag === 'Transition' ||
    node.tag === 'KeepAlive' ||
    node.tag === 'keep-alive')
) {
  const rootChildren = filterChild(context.root)
  if (rootChildren.length === 1 && rootChildren[0] === node) {
    if (hasSingleChild(node)) {
      injectFallthroughAttrs(node.children[0])
    }
    return
  }
}

逻辑分解:

  • 若当前节点是一个特殊的 Vue 内建组件(如 <transition><keep-alive>);
  • 且该组件是整个模板的唯一根节点;
  • 并且它内部只包含一个实际子节点;
  • 则将 _attrs 透传给那个唯一子节点。

这样保证 SSR 输出的 HTML 与运行时渲染保持一致:根节点 <transition> 自身不会渲染成实际 DOM 元素,属性应传递给内部真实节点。


四、实践:条件渲染与单根节点处理

1. 针对 v-if 分支

if (node.type === NodeTypes.IF_BRANCH && hasSingleChild(node)) {
  let hasEncounteredIf = false
  for (const c of filterChild(parent)) {
    if (
      c.type === NodeTypes.IF ||
      (c.type === NodeTypes.ELEMENT && findDir(c, 'if'))
    ) {
      if (hasEncounteredIf) return
      hasEncounteredIf = true
    } else if (
      !hasEncounteredIf ||
      !(c.type === NodeTypes.ELEMENT && findDir(c, /else/, true))
    ) {
      return
    }
  }
  injectFallthroughAttrs(node.children[0])
}

注释:

  • 如果当前节点是一个 v-if 分支;
  • 且该分支中只有一个有效子节点;
  • 则确认整个模板中只有这一组 v-if / v-else
  • 最终在该子节点上注入 _attrs 绑定。

🔍 这样可避免多 v-if 根节点同时存在导致透传混乱的问题。


2. 单根节点场景

else if (hasSingleChild(parent)) {
  injectFallthroughAttrs(node)
}

若模板整体结构只有一个子节点,则直接为该子节点注入 _attrs


五、拓展:injectFallthroughAttrs 实现细解

function injectFallthroughAttrs(node: RootNode | TemplateChildNode) {
  if (
    node.type === NodeTypes.ELEMENT &&
    (node.tagType === ElementTypes.ELEMENT ||
      node.tagType === ElementTypes.COMPONENT) &&
    !findDir(node, 'for')
  ) {
    node.props.push({
      type: NodeTypes.DIRECTIVE,
      name: 'bind',
      arg: undefined,
      exp: createSimpleExpression(`_attrs`, false),
      modifiers: [],
      loc: locStub,
    })
  }
}

逐行解析:

  1. 仅处理真实元素或组件(跳过模板指令节点、注释节点等)。

  2. 跳过带有 v-for 的节点(因其会复制属性,需单独处理)。

  3. 向节点的 props 数组中追加一个虚拟 v-bind 指令:

    v-bind="_attrs"
    

    实际等价于模板中的:

    <div v-bind="_attrs"></div>
    

结果:

在 SSR 渲染时,_attrs 会展开为组件调用上下文中的属性集合,从而实现“透传到根元素”的效果。


六、潜在问题与注意点

  1. 仅适用于 SSR 编译阶段

    • 在客户端模板编译或 SFC 编译中,这段逻辑不会生效;
    • 仅在服务端渲染(@vue/compiler-ssr)路径中参与 AST 处理。
  2. 与 v-for 互斥

    • 若模板结构中有循环渲染的根元素,应通过手动绑定属性;
    • 否则可能出现属性重复注入或覆盖问题。
  3. 多根模板不支持自动注入

    • SSR 模板若包含多个平级根节点,则不会自动注入 _attrs
    • 因为 Vue SSR 期望一个组件返回单一根元素。
  4. 性能影响

    • filterChildfindDir 的频繁调用在大型模板中略有性能消耗;
    • 但仅发生在编译时,不影响运行时性能。

七、结语

ssrInjectFallthroughAttrs 是 Vue SSR 编译器中非常关键的一个 AST 转换器
它自动将 _attrs 透传到真正的 DOM 根节点,从而确保 SSR 输出与客户端一致。
该逻辑兼顾了多种复杂场景(如 transitionv-if、单根模板等),
展示了 Vue 编译体系在精细性与一致性上的高度工程化设计。


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

Vue SSR 编译阶段中的 ssrInjectCssVars 深度解析

作者 excel
2025年11月9日 07:23

在 Vue 3 的 SSR(Server-Side Rendering)编译流程中,ssrInjectCssVars 是一个关键的 编译时 NodeTransform(节点转换函数) ,用于在服务端渲染阶段自动为组件注入 CSS 变量绑定。这篇文章将带你系统解析其实现原理与逻辑设计。


一、概念层:NodeTransform 与 SSR 上下文

在 Vue 的编译管线中,模板编译主要分为三个阶段:

  1. Parse(解析) :将模板字符串解析为抽象语法树(AST)。
  2. Transform(转换) :对 AST 进行多种节点级转换(NodeTransform),如 v-if、v-for、绑定指令等。
  3. Generate(代码生成) :将 AST 转换为渲染函数代码。

而本文中的:

export const ssrInjectCssVars: NodeTransform = (node, context) => { ... }

定义了一个 节点转换函数(NodeTransform)
当编译器遍历每个 AST 节点时,这个函数会被调用,用于在 SSR 场景下注入 _cssVars


二、原理层:CSS 变量注入的编译策略

Vue 在 SSR 时需要保证组件样式中的 v-bind() 变量依然能在服务端生成正确的样式。

在客户端渲染时,v-bind(color) 等样式绑定通过响应式系统实时更新;
但 SSR 阶段是静态的,因此需要提前在渲染函数中生成 _cssVars 对象,并自动注入每个元素:

{ ..._cssVars }

这正是 ssrInjectCssVars 所做的工作 —— 在模板 AST 上注入对 _cssVars 的绑定指令


三、源码层:核心逻辑逐行解析

下面我们逐段讲解源码的实现逻辑。


1. 引入依赖

import {
  ElementTypes,
  type NodeTransform,
  NodeTypes,
  type RootNode,
  type TemplateChildNode,
  createSimpleExpression,
  findDir,
  locStub,
} from '@vue/compiler-dom'
  • NodeTransform:节点转换函数的类型定义。
  • NodeTypes / ElementTypes:AST 节点类型枚举。
  • createSimpleExpression:创建简单表达式节点(用于绑定表达式)。
  • findDir:用于检测某个节点上是否存在特定指令(如 v-for)。
  • locStub:一个空的位置信息对象,用于简化生成 AST 节点时的定位需求。

2. 定义转换函数主体

export const ssrInjectCssVars: NodeTransform = (node, context) => {
  if (!context.ssrCssVars) {
    return
  }
  • 检查 context.ssrCssVars:如果没有定义 CSS 变量上下文(说明不需要注入),则直接返回。

3. 注册 _cssVars 变量

  if (node.type === NodeTypes.ROOT) {
    context.identifiers._cssVars = 1
  }
  • 当节点为根节点(ROOT)时,注册 _cssVars 标识符到 context.identifiers
  • 意味着 _cssVars 会在生成的 SSR 渲染函数中作为全局变量被引用。

4. 仅在根层元素注入

  const parent = context.parent
  if (!parent || parent.type !== NodeTypes.ROOT) {
    return
  }
  • 如果当前节点不是根节点的直接子节点,则不注入。
  • 这可以避免在嵌套组件或局部模板中重复添加。

5. 处理条件分支节点

  if (node.type === NodeTypes.IF_BRANCH) {
    for (const child of node.children) {
      injectCssVars(child)
    }
  } else {
    injectCssVars(node)
  }
}
  • 若当前节点是 v-if / v-else 的分支,则需对每个分支子节点递归注入。
  • 否则直接调用 injectCssVars 处理当前节点。

四、函数层:injectCssVars 注入逻辑

核心函数如下:

function injectCssVars(node: RootNode | TemplateChildNode) {
  if (
    node.type === NodeTypes.ELEMENT &&
    (node.tagType === ElementTypes.ELEMENT ||
      node.tagType === ElementTypes.COMPONENT) &&
    !findDir(node, 'for')
  ) {

(1) 条件判断逻辑

  • 仅处理普通元素或组件节点;
  • 跳过带 v-for 的节点(因为循环会单独生成作用域变量,不应直接注入)。

(2) 特殊处理 suspense 节点

    if (node.tag === 'suspense' || node.tag === 'Suspense') {
      for (const child of node.children) {
        if (
          child.type === NodeTypes.ELEMENT &&
          child.tagType === ElementTypes.TEMPLATE
        ) {
          // suspense slot
          child.children.forEach(injectCssVars)
        } else {
          injectCssVars(child)
        }
      }
    }
  • Suspense 组件内部的模板结构特殊,需深入遍历其子模板层。
  • <template> 插槽内容(fallback 或 default)进行递归注入。

(3) 默认注入逻辑

    else {
      node.props.push({
        type: NodeTypes.DIRECTIVE,
        name: 'bind',
        arg: undefined,
        exp: createSimpleExpression(`_cssVars`, false),
        modifiers: [],
        loc: locStub,
      })
    }
  }
}

这一段是真正注入 _cssVars 的地方。

  • 通过 node.props.push() 在当前节点属性中插入一个新的绑定指令:

    v-bind="_cssVars"
    
  • 即在生成的 SSR 渲染代码中,这个元素会被渲染为:

    <div {..._cssVars}>
    

这使得 SSR 渲染的元素在输出 HTML 时携带正确的样式绑定信息。


五、对比层:与客户端编译的差异

场景 客户端编译 (Client) 服务端编译 (SSR)
CSS 变量来源 响应式系统动态计算 编译期静态注入 _cssVars
更新方式 响应式更新 DOM 无需更新(一次性输出)
实现手段 runtime binding AST 编译时注入

因此 ssrInjectCssVars 的存在意义在于:
将客户端的动态响应式样式“前移”为 SSR 的静态模板注入。


六、实践层:示例演示

模板输入

<template>
  <div class="box">
    <span>Hello</span>
  </div>
</template>

SSR 编译后(概念示例)

function ssrRender(_ctx, _push, _parent, _attrs) {
  _push(`<div ${ssrRenderAttrs(_cssVars)}>`)
  _push(`<span>Hello</span></div>`)
}

可以看到,_cssVars 被自动注入到根级元素中,用于生成内联样式。


七、拓展层:与其他编译阶段的协作

  • ssrCodegenTransform:负责在 SSR 代码生成阶段初始化 _cssVars
  • ssrInjectCssVars:负责在 AST 阶段注入引用。
  • ssrRenderAttrs:在最终渲染时,将 _cssVars 转换为 HTML 属性字符串。

三者协作完成了 SSR 样式变量的完整注入链。


八、潜在问题与优化思考

  1. 冗余注入:若模板层级复杂,可能会在多处节点重复注入 _cssVars
  2. Suspense 嵌套性能:深层递归注入可能影响 SSR 编译性能。
  3. v-for 与 scope 冲突:被跳过的 v-for 节点可能遗漏样式变量覆盖。

未来可以考虑通过 AST 缓存 + 节点标记 优化多次递归注入问题。


九、总结

ssrInjectCssVars 是 Vue SSR 编译管线中的一个 关键 NodeTransform,通过在 AST 阶段为根节点及子元素自动注入 _cssVars,实现了样式变量在服务端的静态绑定,从而保持了 SSR 与客户端渲染的一致性。


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

每日一题-得到 0 的操作数🟢

2025年11月9日 00:00

给你两个 非负 整数 num1num2

每一步 操作 中,如果 num1 >= num2 ,你必须用 num1num2 ;否则,你必须用 num2num1

  • 例如,num1 = 5num2 = 4 ,应该用 num1num2 ,因此,得到 num1 = 1num2 = 4 。然而,如果 num1 = 4num2 = 5 ,一步操作后,得到 num1 = 4num2 = 1

返回使 num1 = 0num2 = 0操作数

 

示例 1:

输入:num1 = 2, num2 = 3
输出:3
解释:
- 操作 1 :num1 = 2 ,num2 = 3 。由于 num1 < num2 ,num2 减 num1 得到 num1 = 2 ,num2 = 3 - 2 = 1 。
- 操作 2 :num1 = 2 ,num2 = 1 。由于 num1 > num2 ,num1 减 num2 。
- 操作 3 :num1 = 1 ,num2 = 1 。由于 num1 == num2 ,num1 减 num2 。
此时 num1 = 0 ,num2 = 1 。由于 num1 == 0 ,不需要再执行任何操作。
所以总操作数是 3 。

示例 2:

输入:num1 = 10, num2 = 10
输出:1
解释:
- 操作 1 :num1 = 10 ,num2 = 10 。由于 num1 == num2 ,num1 减 num2 得到 num1 = 10 - 10 = 0 。
此时 num1 = 0 ,num2 = 10 。由于 num1 == 0 ,不需要再执行任何操作。
所以总操作数是 1 。

 

提示:

  • 0 <= num1, num2 <= 105

得到 0 的操作数

2022年2月16日 17:40

方法一:辗转相除

思路与算法

我们可以按要求模拟两数比较后相减的操作,但在两数差距十分悬殊时,会有大量连续且相同的相减操作,因此我们可以对模拟的过程进行优化。

不妨假设 $\textit{num}_1 \ge \textit{num}_2$,则我们需要用 $\textit{num}_1$ 减 $\textit{num}_2$,直到 $\textit{num}_1 < \textit{num}_2$ 为止。当上述一系列操作结束之后,$\textit{num}_1$ 的新数值即为 $\textit{num}_1 \bmod \textit{num}_2$,在这期间进行了 $\lfloor \textit{num}_1 / \textit{num}_2 \rfloor$ 次相减操作,其中 $\lfloor\dots\rfloor$ 代表向下取整。

容易发现,题目中的过程即为求两个数最大公约数的「辗转相减」方法,而我们需要将它优化为时间复杂度更低的「辗转相除」法,同时利用前文的方法统计对应相减操作的次数。

具体而言,在模拟的过程中,我们用 $\textit{res}$ 来统计相减操作的次数。在每一步模拟开始前,我们需要保证 $\textit{num}_1 \ge \textit{num}_2$;在每一步中,两个数 $(\textit{num}_1, \textit{num}_2)$ 变为 $(\textit{num}_1 \bmod \textit{num}_2, \textit{num}_2)$,同时我们将 $\textit{res}$ 加上该步中相减操作的次数 $\lfloor \textit{num}_1 / \textit{num}_2 \rfloor$;最后,我们还需要交换 $\textit{num}_1$ 与 $\textit{num}_2$ 的数值,以保证下一步模拟的初始条件。当 $\textit{num}_1$ 或 $\textit{num}_2$ 中至少有一个数字为零时,循环结束,我们返回 $\textit{res}$ 作为答案。

细节

在第一步模拟(进入循环)之前,我们事实上不需要保证 $\textit{num}_1 \ge \textit{num}_2$,因为我们可以通过额外的一步模拟将 $(\textit{num}_1, \textit{num}_2)$ 变为 $(\textit{num}_2, \textit{num}_1)$,而这一步贡献的相减次数为 $0$。

代码

###C++

class Solution {
public:
    int countOperations(int num1, int num2) {
        int res = 0;   // 相减操作的总次数
        while (num1 && num2) {
            // 每一步辗转相除操作
            res += num1 / num2;
            num1 %= num2;
            swap(num1, num2);
        }
        return res;
    }
};

###Python

class Solution:
    def countOperations(self, num1: int, num2: int) -> int:
        res = 0   # 相减操作的总次数
        while num1 and num2:
            # 每一步辗转相除操作
            res += num1 // num2
            num1 %= num2
            num1, num2 = num2, num1
        return res

###Java

class Solution {
    public int countOperations(int num1, int num2) {
        int res = 0;   // 相减操作的总次数
        while (num1 != 0 && num2 != 0) {
            // 每一步辗转相除操作
            res += num1 / num2;
            num1 %= num2;
            // 交换两个数
            int temp = num1;
            num1 = num2;
            num2 = temp;
        }
        return res;
    }
}

###C#

public class Solution {
    public int CountOperations(int num1, int num2) {
        int res = 0;   // 相减操作的总次数
        while (num1 != 0 && num2 != 0) {
            // 每一步辗转相除操作
            res += num1 / num2;
            num1 %= num2;
            // 交换两个数
            (num1, num2) = (num2, num1);
        }
        return res;
    }
}

###Go

func countOperations(num1 int, num2 int) int {
    res := 0   // 相减操作的总次数
    for num1 != 0 && num2 != 0 {
        // 每一步辗转相除操作
        res += num1 / num2
        num1 %= num2
        // 交换两个数
        num1, num2 = num2, num1
    }
    return res
}

###C

int countOperations(int num1, int num2) {
    int res = 0;   // 相减操作的总次数
    while (num1 && num2) {
        // 每一步辗转相除操作
        res += num1 / num2;
        num1 %= num2;
        // 交换两个数
        int temp = num1;
        num1 = num2;
        num2 = temp;
    }
    return res;
}

###JavaScript

var countOperations = function(num1, num2) {
    let res = 0;   // 相减操作的总次数
    while (num1 && num2) {
        // 每一步辗转相除操作
        res += Math.floor(num1 / num2);
        num1 %= num2;
        // 交换两个数
        [num1, num2] = [num2, num1];
    }
    return res;
};

###TypeScript

function countOperations(num1: number, num2: number): number {
    let res = 0;   // 相减操作的总次数
    while (num1 && num2) {
        // 每一步辗转相除操作
        res += Math.floor(num1 / num2);
        num1 %= num2;
        // 交换两个数
        [num1, num2] = [num2, num1];
    }
    return res;
};

###Rust

impl Solution {
    pub fn count_operations(mut num1: i32, mut num2: i32) -> i32 {
        let mut res = 0;   // 相减操作的总次数
        while num1 != 0 && num2 != 0 {
            // 每一步辗转相除操作
            res += num1 / num2;
            num1 %= num2;
            // 交换两个数
            std::mem::swap(&mut num1, &mut num2);
        }
        res
    }
}

复杂度分析

  • 时间复杂度:$O(\log \max(\textit{num}_1, \textit{num}_2))$。即为模拟辗转相除并统计操作次数的时间复杂度。

  • 空间复杂度:$O(1)$。

O(log) 辗转相除(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2022年2月13日 12:13

下文把 $\textit{num}_1$ 和 $\textit{num}_2$ 分别记作 $x$ 和 $y$。

根据题意,如果 $x\ge y$,那么 $x$ 要不断减去 $y$,直到小于 $y$。这和商的定义是一样的,所以把 $x$ 减少到小于 $y$ 的操作数为 $\left\lfloor\dfrac{x}{y}\right\rfloor$。

$x<y$ 后,$x$ 变成 $x\bmod y$。

我们可以交换 $x$ 和 $y$,重复上述过程,这样无需实现 $x<y$ 时把 $y$ 变小的逻辑。

循环直到 $y=0$ 为止。

累加所有操作数,即为答案。

class Solution:
    def countOperations(self, x: int, y: int) -> int:
        ans = 0
        while y > 0:
            ans += x // y  # x 变成 x%y
            x, y = y, x % y
        return ans
class Solution {
    public int countOperations(int x, int y) {
        int ans = 0;
        while (y > 0) {
            ans += x / y;
            int r = x % y; // x 变成 r
            x = y; // 交换 x 和 y
            y = r;
        }
        return ans;
    }
}
class Solution {
public:
    int countOperations(int x, int y) {
        int ans = 0;
        while (y > 0) {
            ans += x / y;
            x %= y;
            swap(x, y);
        }
        return ans;
    }
};
int countOperations(int x, int y) {
    int ans = 0;
    while (y > 0) {
        ans += x / y;
        int r = x % y; // x 变成 r
        x = y; // 交换 x 和 y
        y = r;
    }
    return ans;
}
func countOperations(x, y int) (ans int) {
for y > 0 {
ans += x / y // x 变成 x%y
x, y = y, x%y
}
return
}
var countOperations = function(x, y) {
    let ans = 0;
    while (y > 0) {
        ans += Math.floor(x / y); // x 变成 x%y
        [x, y] = [y, x % y];
    }
    return ans;
};
impl Solution {
    pub fn count_operations(mut x: i32, mut y: i32) -> i32 {
        let mut ans = 0;
        while y > 0 {
            ans += x / y; // x 变成 x%y
            (x, y) = (y, x % y);
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(\log\max(x,y))$。当 $x$ 和 $y$ 为斐波那契数列中的相邻两项时,达到最坏情况。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

⚙️ 一次性警告机制的实现:warnOnce 源码深度解析

作者 excel
2025年11月8日 21:51

在开发框架或构建工具时,我们常常需要在运行时输出警告信息(例如 API 弃用提示、错误使用警告等)。
但如果同一条警告反复出现,就会严重干扰开发者的调试体验。
本文将通过 Vue 源码中的 warnOncewarn 函数,带你理解一个「只警告一次」的设计模式。


一、概念层:warnOnce 是什么?

warnOnce 是一个一次性警告输出函数
它在开发环境下输出带有特定格式的警告信息,但会确保同一条消息只打印一次,避免重复噪音。

warn 是其底层依赖,用于实际打印彩色警告信息。


二、原理层:核心机制解析

核心逻辑分为三步:

  1. 用字典缓存已警告消息
    利用一个对象 hasWarned 来记录哪些消息已经输出过;
  2. 根据运行环境判断是否输出
    在生产环境 (process.env.NODE_ENV === 'production') 或测试环境 (__TEST__) 中不输出;
  3. 彩色打印消息
    通过 console.warn 与 ANSI 转义序列,使终端输出带颜色的文字。

三、源码层:逐行讲解

const hasWarned: Record<string, boolean> = {}

作用:定义一个全局记录表,用来存储已输出过的警告信息。
原理

  • Record<string, boolean> 表示键为字符串、值为布尔类型的对象。
  • msg 作为键存在时,说明该警告已经打印过。

export function warnOnce(msg: string): void {
  const isNodeProd =
    typeof process !== 'undefined' && process.env.NODE_ENV === 'production'

解释:判断当前是否处于生产环境。

  • typeof process !== 'undefined' 确保在浏览器环境中不会报错。
  • process.env.NODE_ENV === 'production' 是标准的 Node 环境生产模式判断。

  if (!isNodeProd && !__TEST__ && !hasWarned[msg]) {
    hasWarned[msg] = true
    warn(msg)
  }
}

逻辑分析:

  • !isNodeProd → 确保不是生产模式;
  • !__TEST__ → 确保不是测试模式;
  • !hasWarned[msg] → 确保消息未重复;

满足以上三条件才会输出,并记录 hasWarned[msg] = true
之后再次调用 warnOnce(msg) 时,消息会被跳过,从而只输出一次。


export function warn(msg: string): void {
  console.warn(
    `\x1b[1m\x1b[33m[@vue/compiler-sfc]\x1b[0m\x1b[33m ${msg}\x1b[0m\n`,
  )
}

作用:统一格式化输出警告。
解析 ANSI 转义码:

转义码 含义
\x1b[1m 启用加粗
\x1b[33m 设置黄色字体
\x1b[0m 重置样式

输出效果类似:

[@vue/compiler-sfc]  ⚠️  某个编译警告

这样开发者一眼就能识别来自 Vue 编译器的提示。


四、对比层:与常规 console.warn 的区别

特性 console.warn warnOnce
输出次数 每次调用都会输出 相同消息仅输出一次
环境控制 自动屏蔽生产与测试环境
格式样式 默认系统样式 自定义加粗+黄字+标签前缀
去重机制 基于消息缓存

因此,warnOnce 更适合框架层警告或插件开发使用。


五、实践层:如何在项目中使用

✅ 示例:避免重复 API 弃用警告

function useOldAPI() {
  warnOnce('useOldAPI() 已弃用,请使用 useNewAPI()')
  // ...旧逻辑
}

运行后,控制台仅会出现一次警告:

[@vue/compiler-sfc] useOldAPI() 已弃用,请使用 useNewAPI()

后续再次调用 useOldAPI() 将不会重复输出。


六、拓展层:可改进的地方

  1. 支持分级警告

    • 可扩展参数 level: 'warn' | 'error' | 'info'
  2. 浏览器颜色兼容

    • 在浏览器端可使用 %c 样式标记,如:

      console.warn('%c[MyLib]', 'color: orange; font-weight: bold;', msg)
      
  3. 持久化警告记录

    • hasWarned 存入 localStorage,以避免刷新后重复。

七、潜在问题与注意事项

  1. 警告信息唯一性依赖 msg 字符串
    若两个不同警告内容文本相同,会被误判为重复。
    ✅ 建议:使用模板字符串增加上下文标识。
  2. 生产模式判断依赖构建工具
    需确保打包工具(如 Vite、Webpack)正确替换 process.env.NODE_ENV
  3. 全局状态不可重置
    若想重新打印警告(例如测试场景),需要手动清空 hasWarned

总结

warnOnce 是一个小巧却极其实用的函数,体现了框架级开发中的三个关键理念:

  • 开发体验优化:防止控制台被重复消息淹没;
  • 🧩 环境感知:智能屏蔽生产/测试模式;
  • 🎨 统一输出规范:提高可读性与辨识度。

在你自己的库或工具中,也可以参考这一设计来构建轻量的日志与警告系统。


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

🧩 使用 Babel + MagicString 实现动态重写 export default 的通用方案

作者 excel
2025年11月8日 21:48

一、背景与概念

在 Vue 或 Vite 等编译器工具链中,我们经常需要在不破坏原始脚本语义的前提下,对模块的默认导出 (export default) 进行重写,以便:

  • 插入运行时代码(如热更新逻辑、注入编译上下文)。
  • 提取默认导出的对象供编译器处理。
  • 动态包装 export default 以便进一步扩展。

而直接用正则或字符串替换很容易破坏代码结构,尤其在包含注释、模板字符串、多行声明的情况下。因此,更安全的方式是:

✅ 使用 @babel/parser 构建 AST
✅ 用 MagicString 实现精准的源码替换。

本文即通过分析 rewriteDefault() 函数的源码,完整讲解一个健壮的默认导出重写实现。


二、整体架构与原理

核心逻辑流程

flowchart TD
  A[parse input via Babel] --> B[traverse AST]
  B --> C{has export default?}
  C -->|No| D[append const as = {}]
  C -->|Yes| E[rewriteDefaultAST]
  E --> F[MagicString overwrite specific nodes]
  F --> G[return rewritten string]

设计思想

  1. 首先用 @babel/parser 解析源代码为 AST;
  2. 判断是否存在 export default
  3. 根据导出类型(class / object / re-export)进行不同的替换策略;
  4. 使用 MagicString 保留源代码位置与映射信息,安全地覆盖文本。

三、核心函数逐段解析

1. rewriteDefault():入口函数

export function rewriteDefault(
  input: string,
  as: string,
  parserPlugins?: ParserPlugin[],
): string {
  const ast = parse(input, {
    sourceType: 'module',
    plugins: resolveParserPlugins('js', parserPlugins),
  }).program.body
  const s = new MagicString(input)

  rewriteDefaultAST(ast, s, as)

  return s.toString()
}

🔍 逐行注释说明

  • parse():使用 Babel 将输入的 JS/TS 源码转成 AST。
  • resolveParserPlugins():根据文件类型选择合适的 Babel 插件组合(例如支持 TS、装饰器、JSX 等)。
  • MagicString(input):实例化一个可变字符串对象,支持精准字符级操作。
  • rewriteDefaultAST():真正执行默认导出重写逻辑。
  • s.toString():返回重写后的源代码字符串。

👉 总结
rewriteDefault() 是一个纯函数式封装,负责连接 Babel 与 MagicString 的桥梁。


2. rewriteDefaultAST():核心重写逻辑

export function rewriteDefaultAST(
  ast: Statement[],
  s: MagicString,
  as: string,
): void {
  if (!hasDefaultExport(ast)) {
    s.append(`\nconst ${as} = {}`)
    return
  }

  ast.forEach(node => {
    if (node.type === 'ExportDefaultDeclaration') {
      // ...
    } else if (node.type === 'ExportNamedDeclaration') {
      // ...
    }
  })
}

🔍 核心逻辑说明

  1. 无默认导出情况
    直接在代码末尾追加一行:

    const <as> = {}
    

    这保证了调用方总能获得一个具名对象(即使源码没有默认导出)。

  2. 存在默认导出时
    遍历所有 AST 节点,处理两类情况:

    • ExportDefaultDeclaration:显式 export default
    • ExportNamedDeclaration:命名导出中导出 default

3. 处理 export default class

if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
  const start = node.declaration.decorators?.length
    ? node.declaration.decorators.at(-1)!.end!
    : node.start!
  s.overwrite(start, node.declaration.id.start!, ` class `)
  s.append(`\nconst ${as} = ${node.declaration.id.name}`)
}

🔍 逻辑说明

  • 如果 export default class Foo {},需要:

    1. 去掉 export default
    2. 保留 class Foo;
    3. 追加 const as = Foo

💡 细节:

  • 装饰器支持:如果类带有装饰器(@xxx),需从装饰器末尾起覆盖。
  • 通过 .overwrite() 精确修改字符串区间。

4. 处理 export default {...} 或表达式

s.overwrite(node.start!, node.declaration.start!, `const ${as} = `)

这将把:

export default { a: 1 }

重写为:

const __VUE_DEFAULT__ = { a: 1 }

5. 处理 export { default } from 'module'

if (node.source) {
  s.prepend(`import { default as __VUE_DEFAULT__ } from '${node.source.value}'\n`)
  s.remove(specifier.start!, end)
  s.append(`\nconst ${as} = __VUE_DEFAULT__`)
}

🔍 行为说明

对于:

export { default } from './foo'

会被转换为:

import { default as __VUE_DEFAULT__ } from './foo'
const __VUE_DEFAULT__ = __VUE_DEFAULT__

这样做的目的是统一所有“默认导出来源”的写法,方便统一注入逻辑。


四、辅助函数解析

hasDefaultExport()

export function hasDefaultExport(ast: Statement[]): boolean {
  for (const stmt of ast) {
    if (stmt.type === 'ExportDefaultDeclaration') return true
    if (
      stmt.type === 'ExportNamedDeclaration' &&
      stmt.specifiers.some(spec => (spec.exported as Identifier).name === 'default')
    ) return true
  }
  return false
}

👉 功能:判断当前 AST 是否包含默认导出。
这在多层导出(如 export { default } from ...)的场景中非常关键。


specifierEnd()

function specifierEnd(s: MagicString, end: number, nodeEnd: number | null) {
  let hasCommas = false
  let oldEnd = end
  while (end < nodeEnd!) {
    if (/\s/.test(s.slice(end, end + 1))) {
      end++
    } else if (s.slice(end, end + 1) === ',') {
      end++
      hasCommas = true
      break
    } else if (s.slice(end, end + 1) === '}') {
      break
    }
  }
  return hasCommas ? end : oldEnd
}

👉 功能
用于确定 export { default, foo } 中的分隔位置,以正确删除单个 default 导出项而不破坏语法。


五、实践与应用场景

Vue SFC 编译阶段
@vue/compiler-sfc 中,rewriteDefault() 被用于将 <script> 块的默认导出转换为变量声明,方便在其上注入编译元信息(如 __scopeId__file 等)。

构建工具插件
在 Rollup/Vite 插件中,也可用相同逻辑动态重写导出结构,实现自定义运行时行为注入。


六、拓展与潜在问题

🧠 拓展方向

  • 处理 TypeScript export default interface(当前未支持)。
  • 支持顶层 await 或动态导入语句的上下文保持。
  • 输出 sourcemap,以便调试与错误追踪。

⚠️ 潜在问题

  1. 当代码包含复杂注释或字符串模板时,MagicString 的边界判断需谨慎。
  2. 对于多层嵌套 export { default as X } 情况,可能需要二次解析。

七、总结

rewriteDefault() 展示了一个高可靠的 AST 操作范式:

语法解析 → 精准修改 → 保持结构完整 → 支持多种导出模式。

这种模式广泛用于现代构建器(如 Vite、Vue、SvelteKit),是源码变换的重要基石。


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

Vue SFC 解析器源码深度解析:从结构设计到源码映射

作者 excel
2025年11月8日 21:46

一、背景与概念

在 Vue 生态中,单文件组件(SFC, Single File Component) 是构建现代前端应用的核心形态。
一个 .vue 文件通常由 <template><script><style> 等块组成。为了让编译器和构建工具(如 Vite、Vue Loader)能正确处理这些结构,就需要一个可靠的 解析器 (parser).vue 文件解析成结构化的抽象表示。

本文将深度剖析 Vue 官方的 SFC 解析逻辑实现,重点讲解:

  • 如何拆解 .vue 文件;
  • 如何生成 SFCDescriptor
  • 如何维护缓存;
  • 如何生成 SourceMap;
  • 以及 HMR(热更新)时的判定逻辑。

二、整体结构与模块导入

import {
  NodeTypes, createRoot, type RootNode, type CompilerError, ...
} from '@vue/compiler-core'
import * as CompilerDOM from '@vue/compiler-dom'
import { SourceMapGenerator } from 'source-map-js'
import { createCache } from './cache'
import { parseCssVars } from './style/cssVars'
import { isImportUsed } from './script/importUsageCheck'
import { genCacheKey } from '@vue/shared'

解析:

  • @vue/compiler-core 提供基础的 AST 节点类型与工具;
  • @vue/compiler-dom 是具体的 DOM 层编译器实现;
  • source-map-js 用于生成源码映射;
  • createCache 是自定义的 LRU 缓存;
  • parseCssVars 负责提取样式块中的 CSS 变量;
  • isImportUsed 用于检测模板中是否使用了某个导入(影响热更新逻辑)。

三、核心数据结构:SFCDescriptor 与 SFCBlock

export interface SFCDescriptor {
  filename: string
  source: string
  template: SFCTemplateBlock | null
  script: SFCScriptBlock | null
  scriptSetup: SFCScriptBlock | null
  styles: SFCStyleBlock[]
  customBlocks: SFCBlock[]
  cssVars: string[]
  slotted: boolean
  shouldForceReload: (prevImports: Record<string, ImportBinding>) => boolean
}

原理说明:

SFCDescriptor 是解析结果的核心结构,表示整个 .vue 文件的抽象描述:

  • 每个 <template><script><style> 等块都对应一个 SFCBlock
  • shouldForceReload() 用于判断是否需要热重载;
  • cssVarsslotted 是编译优化标记。

对应的 SFCBlock 则表示单个区块的信息:

export interface SFCBlock {
  type: string
  content: string
  attrs: Record<string, string | true>
  loc: SourceLocation
  map?: RawSourceMap
  lang?: string
  src?: string
}

四、parse() 主流程解析

export function parse(source: string, options: SFCParseOptions = {}): SFCParseResult {
  const sourceKey = genCacheKey(source, {...options})
  const cache = parseCache.get(sourceKey)
  if (cache) return cache

1️⃣ 缓存策略

解析首先生成唯一的 cacheKey,通过 genCacheKey() 基于源码与配置计算出哈希键。
若缓存命中,直接返回,避免重复解析。


2️⃣ 初始化参数与描述对象

const descriptor: SFCDescriptor = {
  filename, source, template: null, script: null, scriptSetup: null,
  styles: [], customBlocks: [], cssVars: [], slotted: false,
  shouldForceReload: prev => hmrShouldReload(prev, descriptor)
}

这是最终返回的核心结构体。


3️⃣ 解析 AST

const ast = compiler.parse(source, {
  parseMode: 'sfc',
  prefixIdentifiers: true,
  ...templateParseOptions,
  onError: e => errors.push(e)
})

使用 @vue/compiler-domparse() 将整个文件转成 DOM AST,并记录语法错误。


4️⃣ 遍历顶层节点

ast.children.forEach(node => {
  if (node.type !== NodeTypes.ELEMENT) return
  switch (node.tag) {
    case 'template': ...
    case 'script': ...
    case 'style': ...
    default: ...
  }
})

按标签类型分派处理逻辑。


五、createBlock():节点转区块对象

function createBlock(node: ElementNode, source: string, pad: boolean) {
  const type = node.tag
  const loc = node.innerLoc!
  const attrs: Record<string, string | true> = {}

  const block: SFCBlock = {
    type,
    content: source.slice(loc.start.offset, loc.end.offset),
    loc,
    attrs,
  }

  node.props.forEach(p => {
    if (p.type === NodeTypes.ATTRIBUTE) {
      attrs[p.name] = p.value ? p.value.content || true : true
    }
  })
  return block
}

注释说明:

  • loc 指定源代码中的位置范围;
  • content 提取该块的实际内容;
  • attrs 收集标签属性;
  • 若有 pad 参数(行或空格填充),则执行 padContent() 保持源码行号对应。

六、SourceMap 生成逻辑

function generateSourceMap(
  filename, source, generated, sourceRoot, lineOffset, columnOffset
): RawSourceMap {
  const map = new SourceMapGenerator({ file: filename, sourceRoot })
  map.setSourceContent(filename, source)
  generated.split(/\r?\n/g).forEach((line, index) => {
    if (!/^(?://)?\s*$/.test(line)) {
      const originalLine = index + 1 + lineOffset
      const generatedLine = index + 1
      for (let i = 0; i < line.length; i++) {
        if (!/\s/.test(line[i])) {
          map._mappings.add({
            originalLine,
            originalColumn: i + columnOffset,
            generatedLine,
            generatedColumn: i,
            source: filename,
            name: null,
          })
        }
      }
    }
  })
  return map.toJSON()
}

原理说明:

  • 通过逐行对比 source 与生成代码内容;
  • 仅映射非空白字符;
  • 使用 SourceMapGenerator 构造出精确的字符级映射。

七、热更新判定逻辑:hmrShouldReload()

export function hmrShouldReload(prevImports, next): boolean {
  if (!next.scriptSetup || !['ts', 'tsx'].includes(next.scriptSetup.lang)) return false
  for (const key in prevImports) {
    if (!prevImports[key].isUsedInTemplate && isImportUsed(key, next)) {
      return true
    }
  }
  return false
}

原理拆解:

  • 仅针对 TypeScript 的 <script setup> 块;
  • 若之前未使用的导入在模板中被使用,则触发全量重载;
  • 目的是避免模板变更导致脚本导入状态不一致。

八、辅助工具函数

  • isEmpty(node) :检查节点是否只有空白文本;
  • hasSrc(node) :检测是否带有 src 属性;
  • dedent() :去除模板缩进;
  • padContent() :行号填充,保证错误映射准确。

这些函数共同保证了 .vue 文件在不同语法情况下的正确解析与映射。


九、拓展与对比

功能点 Vue 3 SFC Parser Vue 2 SFC Parser
AST 来源 @vue/compiler-dom 独立 HTML Parser
缓存机制 LRU 缓存 + Hash Key
<script setup> 支持
CSS Vars 提取
SourceMap 精度 字符级 行级

Vue 3 的 SFC 解析器在 可扩展性、性能和开发体验 上都进行了重构,尤其配合 Vite 的即时编译与热更新能力,实现了模块级粒度的重载。


十、潜在问题与优化方向

  1. SourceMap 生成效率
    当前逐字符映射在大文件时较慢,可考虑行级快速映射与差量缓存。
  2. 多语言 Template 支持
    如 Pug/Jade 模板处理仍依赖 dedent(),可扩展到更通用的缩进推断。
  3. 自定义 Block 扩展性
    <docs><test> 等自定义块支持较弱,可通过插件机制增强。

十一、总结

Vue 的 SFC 解析器是一个集 AST 分析、区块抽象、缓存与 SourceMap 生成于一体的系统组件。
它将 .vue 文件结构化为统一的 SFCDescriptor,为编译器、构建工具和 HMR 提供了强大的底层支撑。

核心价值:
让前端开发者以单文件形式组织组件,而编译器与构建工具能高效理解、编译与热更新。


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

Vue SFC 编译器主导出文件解析:模块组织与设计哲学

作者 excel
2025年11月8日 21:44

本文解析的是 Vue 单文件组件(SFC)编译器核心入口文件的一部分,其作用是统一导出 SFC 编译体系中的主要功能模块与类型定义
该文件在 @vue/compiler-sfc 中扮演“门面(Facade)”角色,连接底层解析、编译、类型推断与错误处理等功能。


一、总体结构概览

export const version: string = __VERSION__

🧩 概念

这一行声明了模块版本常量,用于框架或工具在运行时检测当前编译器版本。

⚙️ 原理

__VERSION__ 是由打包工具(如 Rollup、Vite)在构建时通过全局替换注入的宏变量。

🔍 对比

类似于:

  • process.env.VERSION(Node 环境变量)
  • React 中的 React.version 静态字段。

🧪 实践

在插件开发中,通常用于判断当前编译器是否与框架版本兼容:

if (compiler.version.startsWith('3.5')) {
  // 做兼容处理
}

二、核心 API 导出

export { parse } from './parse'
export { compileTemplate } from './compileTemplate'
export { compileStyle, compileStyleAsync } from './compileStyle'
export { compileScript } from './compileScript'

🧩 概念

这几项导出构成了 SFC 编译的核心阶段:

函数 作用
parse 解析 .vue 文件结构,生成 AST 描述符
compileTemplate <template> 编译为渲染函数
compileStyle 编译 <style>,支持作用域与 PostCSS 处理
compileScript 处理 <script>,包括 TS 与 <script setup> 转换

⚙️ 原理

Vue 的 SFC 编译流程可抽象为:

SFC 文件 → parse() → descriptor
descriptor → compileTemplate / compileScript / compileStyle → JS/CSS 产物

三、内部缓存与错误信息集成

import { type SFCParseResult, parseCache as _parseCache } from './parse'
export const parseCache = _parseCache as Map<string, SFCParseResult>

🧩 概念

parseCache 是解析缓存,用于提高多次编译同文件的性能。

⚙️ 原理

基于 LRU(Least Recently Used)缓存策略,避免重复解析同样内容的文件。

⚠️ 设计注释

注释中提到:“避免暴露 LRU 类型”,表明这是对外部接口封装的一种简化。开发者无需依赖 LRU 的具体实现类型。


四、错误信息统一导出

import {
  DOMErrorMessages,
  errorMessages as coreErrorMessages,
} from '@vue/compiler-dom'

export const errorMessages: Record<number, string> = {
  ...coreErrorMessages,
  ...DOMErrorMessages,
}

🧩 概念

Vue 编译器的错误信息来自多个模块:

  • compiler-core:通用错误,如 AST 结构、指令错误;
  • compiler-dom:浏览器端特有错误,如 HTML 属性校验。

⚙️ 原理

通过对象合并(spread operator),构建一个统一的错误码字典,用于 IDE 或 CLI 在编译阶段展示详细报错。


五、通用工具函数导出

export { parse as babelParse } from '@babel/parser'
import MagicString from 'magic-string'
export { MagicString }
import { walk as _walk } from 'estree-walker'
export const walk = _walk as any

🧩 工具说明

工具 功能 应用场景
babelParse 解析 JS/TS 代码为 AST Script 编译
MagicString 精确源码编辑工具 源码改写、生成 source map
walk AST 遍历 静态分析与标识符提取

💡 实例

import MagicString from 'magic-string'

const code = new MagicString('let a = 1')
code.overwrite(4, 5, 'b') // => let b = 1
console.log(code.toString())

此工具可保持行列信息一致,是代码转换中生成精确映射的关键。


六、类型定义导出

export type {
  SFCParseOptions,
  SFCDescriptor,
  SFCBlock,
  ...
} from './parse'

🧩 概念

类型导出提供 IDE 智能提示与类型检查,确保在 TypeScript 环境中安全使用。

⚙️ 原理

这些类型定义描述了 Vue SFC 编译各阶段的结构:

  • SFCDescriptor:完整 SFC 的语法树表示;
  • SFCBlock:模板、脚本、样式等结构块;
  • SFCTemplateCompileResults:模板编译结果对象。

🔍 对比

与 React 不同,Vue 的编译器在类型系统中深度绑定 AST 结构,使得编辑器插件(如 Volar)能精准推断组件接口。


七、兼容性与废弃处理

/**
 * @deprecated
 */
export const shouldTransformRef = () => false

🧩 概念

该函数是为旧版 vite-plugin-vue (<5.0) 保留的兼容 API。

⚙️ 原理

过去 Vue 支持 reactivityTransform 语法(如 ref: count = 1),后因语义复杂被移除。此处返回 false,即“不再进行转换”,以避免报错。


八、设计思想与模块解耦

⚙️ 原理分析

该入口文件体现了 Vue 编译系统的“三层分工”:

层级 模块 作用
语法层 parse / babelParse 解析源文件与脚本
转换层 compileTemplate / compileScript / compileStyle 代码转换与优化
接口层 导出类型 / 工具 / 错误集 对外暴露统一 API

这种分层设计保障了:

  • 各阶段职责清晰;
  • 可单独测试与替换;
  • 插件生态更易扩展。

九、潜在问题与优化方向

  1. parseCache 的命中率问题
    若缓存策略未调优,可能导致多文件编译时反复 I/O。
  2. MagicString 的内存消耗
    在大型工程中可能影响编译速度,可考虑增量式编辑。
  3. 多模块导出复杂度
    虽方便统一调用,但可能造成包体积膨胀,需要按需 tree-shaking。

🔚 结语

本文分析了 Vue SFC 编译器的主导出模块,重点解释了其API 结构、内部逻辑与设计哲学
它不仅是编译流程的汇总入口,更体现了 Vue 编译系统的模块化与可扩展理念。

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

深度解析:Vue SFC 模板编译器核心实现 (compileTemplate)

作者 excel
2025年11月8日 21:42

一、概念与背景

在 Vue 的单文件组件(SFC, Single File Component)体系中,模板部分(<template>)需要被编译为高效的渲染函数(render function)。
@vue/compiler-sfc 模块正是这一转换的关键执行者,它为 .vue 文件的模板部分提供从 原始字符串 → AST → 渲染代码 的完整流程。

本文聚焦于 compileTemplate 及其相关辅助逻辑,揭示 Vue 如何在构建阶段将模板编译为最终的可执行渲染函数。


二、原理与结构分解

源码核心逻辑包括以下模块:

  1. preprocess
    负责调用模板语言(如 Pug、EJS)进行预处理。
  2. compileTemplate
    外部 API,协调预处理与最终编译。
  3. doCompileTemplate
    实际的编译执行逻辑,调用底层编译器(@vue/compiler-dom@vue/compiler-ssr)。
  4. mapLines / patchErrors
    用于处理 SourceMap 与错误信息的行号映射,使开发时错误定位准确。

三、核心函数讲解

1. preprocess()

function preprocess(
  { source, filename, preprocessOptions }: SFCTemplateCompileOptions,
  preprocessor: PreProcessor,
): string {
  let res: string = ''
  let err: Error | null = null

  preprocessor.render(
    source,
    { filename, ...preprocessOptions },
    (_err, _res) => {
      if (_err) err = _err
      res = _res
    },
  )

  if (err) throw err
  return res
}

功能说明:

  • 使用 @vue/consolidate 统一调用第三方模板引擎(如 Pug、EJS)。
  • 封装成同步调用,以兼容 Jest 测试环境(require hooks 必须同步)。
  • 若预处理失败,抛出错误以便上层捕获。

注释说明:

  • preprocessor.render() 是同步执行的,即便暴露了 callback 风格。
  • 返回的结果为编译后的纯 HTML 字符串。

2. compileTemplate()

export function compileTemplate(
  options: SFCTemplateCompileOptions,
): SFCTemplateCompileResults {
  const { preprocessLang, preprocessCustomRequire } = options

  // 处理浏览器端的预处理约束
  if (
    (__ESM_BROWSER__ || __GLOBAL__) &&
    preprocessLang &&
    !preprocessCustomRequire
  ) {
    throw new Error(
      `[@vue/compiler-sfc] Template preprocessing in the browser build must provide the `preprocessCustomRequire` option...`
    )
  }

  // 加载相应预处理器
  const preprocessor = preprocessLang
    ? preprocessCustomRequire
      ? preprocessCustomRequire(preprocessLang)
      : __ESM_BROWSER__
        ? undefined
        : consolidate[preprocessLang as keyof typeof consolidate]
    : false

  if (preprocessor) {
    try {
      return doCompileTemplate({
        ...options,
        source: preprocess(options, preprocessor),
        ast: undefined,
      })
    } catch (e: any) {
      return {
        code: `export default function render() {}`,
        source: options.source,
        tips: [],
        errors: [e],
      }
    }
  } else if (preprocessLang) {
    return {
      code: `export default function render() {}`,
      source: options.source,
      tips: [
        `Component ${options.filename} uses lang ${preprocessLang}...`,
      ],
      errors: [
        `Component ${options.filename} uses lang ${preprocessLang}, however it is not installed.`,
      ],
    }
  } else {
    return doCompileTemplate(options)
  }
}

逻辑层次:

  1. 检查浏览器环境:浏览器端无法动态加载 Node 模块,必须手动注入。
  2. 根据 lang 选择预处理器:例如 pug@vue/consolidate.pug
  3. 执行预处理 → 调用真正的模板编译函数 doCompileTemplate()
  4. 若预处理器不存在,则返回警告代码与错误提示。

3. doCompileTemplate()

这是整个流程的核心:

function doCompileTemplate({
  filename,
  id,
  scoped,
  slotted,
  inMap,
  source,
  ast: inAST,
  ssr = false,
  ssrCssVars,
  isProd = false,
  compiler,
  compilerOptions = {},
  transformAssetUrls,
}: SFCTemplateCompileOptions): SFCTemplateCompileResults {
  const errors: CompilerError[] = []
  const warnings: CompilerError[] = []

  // 配置资源路径转换
  let nodeTransforms: NodeTransform[] = []
  if (isObject(transformAssetUrls)) {
    const assetOptions = normalizeOptions(transformAssetUrls)
    nodeTransforms = [
      createAssetUrlTransformWithOptions(assetOptions),
      createSrcsetTransformWithOptions(assetOptions),
    ]
  } else if (transformAssetUrls !== false) {
    nodeTransforms = [transformAssetUrl, transformSrcset]
  }

  // SSR 校验
  if (ssr && !ssrCssVars) {
    warnOnce(`compileTemplate is called with `ssr: true` but no cssVars`)
  }

  // 编译器选择:DOM / SSR
  const shortId = id.replace(/^data-v-/, '')
  const longId = `data-v-${shortId}`
  const defaultCompiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM
  compiler = compiler || defaultCompiler

  // 执行编译
  let { code, ast, preamble, map } = compiler.compile(inAST || source, {
    mode: 'module',
    prefixIdentifiers: true,
    hoistStatic: true,
    cacheHandlers: true,
    ssrCssVars:
      ssr && ssrCssVars?.length
        ? genCssVarsFromList(ssrCssVars, shortId, isProd, true)
        : '',
    scopeId: scoped ? longId : undefined,
    sourceMap: true,
    ...compilerOptions,
    nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
    filename,
    onError: e => errors.push(e),
    onWarn: w => warnings.push(w),
  })

  // SourceMap 对齐
  if (inMap && !inAST) {
    if (map) map = mapLines(inMap, map)
    if (errors.length) patchErrors(errors, source, inMap)
  }

  // 生成提示信息
  const tips = warnings.map(w => {
    let msg = w.message
    if (w.loc) {
      msg += `\n${generateCodeFrame(source, w.loc.start.offset, w.loc.end.offset)}`
    }
    return msg
  })

  return { code, ast, preamble, source, errors, tips, map }
}

关键逻辑详解:

步骤 功能 说明
1. nodeTransforms 构建 构造资源路径转换插件链 将模板内 src / srcset 转为 import 路径
2. 编译器选择 DOM vs SSR 根据 ssr 选项使用不同编译器
3. compile 调用 核心编译 调用 @vue/compiler-dom.compile
4. mapLines / patchErrors 调整映射 修正模板行号偏移问题
5. 提示收集 格式化警告输出 利用 generateCodeFrame 高亮源码片段

4. Source Map 处理函数

mapLines()

parse.ts 产生的简单行映射与 compiler-dom 的细粒度映射合并:

function mapLines(oldMap: RawSourceMap, newMap: RawSourceMap): RawSourceMap {
  const oldMapConsumer = new SourceMapConsumer(oldMap)
  const newMapConsumer = new SourceMapConsumer(newMap)
  const mergedMapGenerator = new SourceMapGenerator()

  newMapConsumer.eachMapping(m => {
    const origPosInOldMap = oldMapConsumer.originalPositionFor({
      line: m.originalLine,
      column: m.originalColumn!,
    })
    if (origPosInOldMap.source == null) return

    mergedMapGenerator.addMapping({
      generated: { line: m.generatedLine, column: m.generatedColumn },
      original: { line: origPosInOldMap.line, column: m.originalColumn! },
      source: origPosInOldMap.source,
    })
  })

  return mergedMapGenerator.toJSON()
}

这一步确保最终调试信息(如错误定位、浏览器 SourceMap)与原始 .vue 文件完全对应。


5. 错误定位修正

function patchErrors(errors: CompilerError[], source: string, inMap: RawSourceMap) {
  const originalSource = inMap.sourcesContent![0]
  const offset = originalSource.indexOf(source)
  const lineOffset = originalSource.slice(0, offset).split(/\r?\n/).length - 1
  errors.forEach(err => {
    if (err.loc) {
      err.loc.start.line += lineOffset
      err.loc.start.offset += offset
      if (err.loc.end !== err.loc.start) {
        err.loc.end.line += lineOffset
        err.loc.end.offset += offset
      }
    }
  })
}

该函数将 AST 错误的行号补偿到原文件上下文中,使错误提示对开发者可读。


四、对比分析:Vue 2 vs Vue 3 模板编译差异

特性 Vue 2 Vue 3
模板编译器入口 vue-template-compiler @vue/compiler-sfc
AST 结构 平面 JSON 更强的语义树(RootNode, ElementNode
Scope 处理 静态分析弱 静态提升与缓存
SSR 支持 独立构建 同源共享逻辑(compiler-ssr
SourceMap 基础行映射 精确列级映射(mapLines合并)

五、实践应用:自定义模板编译流程

import { compileTemplate } from '@vue/compiler-sfc'

const result = compileTemplate({
  source: `<div>{{ msg }}</div>`,
  filename: 'App.vue',
  id: 'data-v-abc123',
  scoped: true,
})
console.log(result.code)

输出示例:

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_ctx.msg), 1))
}

这样,我们便得到了从 .vue 文件模板到最终渲染函数的编译结果。


六、拓展与优化

  • 自定义 Node Transform
    可注入自定义 AST 变换逻辑,拓展编译语义。
  • SSR 优化
    提供 ssrCssVars 用于服务端渲染时的样式变量注入。
  • 错误可视化
    借助 generateCodeFrame 实现 IDE 内高亮定位。

七、潜在问题与设计取舍

  1. 同步预处理设计限制
    为兼容 Jest 测试,预处理器需同步执行,这在异步模板语言(如 Handlebars async helper)下存在限制。
  2. SourceMap 性能问题
    多层 map 合并在大型项目中可能带来性能瓶颈。
  3. 跨版本兼容
    Vue 自定义编译器 API 稳定性仍在演化,未来版本可能调整 AST 接口。

结语:
compileTemplate 是 Vue 3 模板编译系统的桥梁层。它将开发者友好的模板语言转换为高效、可调试、可扩展的渲染函数体系。理解其结构,对深入掌握 Vue 编译机制与自定义编译插件开发至关重要。


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

Vue SFC 样式编译核心机制详解:compileStyle 与 PostCSS 管线设计

作者 excel
2025年11月8日 21:40

一、概念:什么是 SFC 样式编译

在 Vue 单文件组件(SFC, Single File Component)体系中,<style> 标签的内容并不仅仅是普通 CSS。
它可能包括:

  • 预处理语言(如 Sass、Less、Stylus)
  • Scoped 样式作用域隔离
  • CSS Modules 模块化支持
  • PostCSS 插件链优化(如变量注入、压缩、自动前缀)

compileStyle 模块就是整个样式编译过程的中枢,负责将 Vue SFC 中的样式块转换为浏览器可执行的标准 CSS,并生成对应的 SourceMap 与依赖追踪。


二、原理:编译流程分层设计

该模块主要围绕三个层次运作:

  1. 接口层(API 层)

    • compileStyle():同步编译
    • compileStyleAsync():异步编译(支持 CSS Modules)
  2. 执行层(核心逻辑)

    • doCompileStyle():统一实现函数,处理 PostCSS 流程、预处理器调用、插件注册等。
  3. 辅助层(工具函数)

    • preprocess():调用对应的预处理器(如 Sass、Less),输出 CSS 与 SourceMap。

这种结构体现了 Vue 样式编译的高扩展性:不同的语言、插件与构建环境都可以通过选项灵活控制。


三、对比:同步与异步模式的区别

特性 compileStyle compileStyleAsync
是否异步
支持 CSS Modules
适用场景 生产构建、本地快速编译 modules: true 或动态加载插件场景
实现方式 直接返回 SFCStyleCompileResults 返回 Promise 形式的结果

Vue 官方明确指出:CSS Modules 必须通过异步方式编译,原因在于 PostCSS 模块插件在执行时需要异步回调(getJSON)以返回模块映射表。


四、实践:源码逐段讲解

1️⃣ 接口定义

export interface SFCStyleCompileOptions {
  source: string        // 样式源码
  filename: string      // 文件名(用于 SourceMap)
  id: string            // Vue 组件唯一 ID (data-v-xxxx)
  scoped?: boolean      // 是否启用 scoped 模式
  trim?: boolean        // 是否启用 CSS 空白裁剪
  preprocessLang?: PreprocessLang // 预处理器语言
  postcssPlugins?: any[]          // PostCSS 插件链
}

💡 id 会决定样式作用域。Vue 在运行时会将 <div> 等 DOM 添加 data-v-xxxx 属性以实现作用域隔离。


2️⃣ 核心编译函数

export function doCompileStyle(options: SFCAsyncStyleCompileOptions) {
  const {
    filename, id, scoped = false, trim = true, isProd = false,
    modules = false, modulesOptions = {}, preprocessLang,
    postcssOptions, postcssPlugins,
  } = options

🧠 这里通过解构抽取关键配置项,允许开发者灵活控制每个阶段的处理逻辑。


3️⃣ 预处理阶段

const preprocessor = preprocessLang && processors[preprocessLang]
const preProcessedSource = preprocessor && preprocess(options, preprocessor)
const map = preProcessedSource ? preProcessedSource.map : options.inMap || options.map
const source = preProcessedSource ? preProcessedSource.code : options.source
  • 若指定 lang="scss",则此处会调用 sass 预处理器。
  • 输出结果包括 codemapdependencies

4️⃣ 插件链组装

const plugins = (postcssPlugins || []).slice()
plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
if (trim) plugins.push(trimPlugin())
if (scoped) plugins.push(scopedPlugin(longId))

逐个插件解释:

插件 功能
cssVarsPlugin 注入 Vue 内联 CSS 变量支持
trimPlugin 清理无意义空白字符
scopedPlugin 为选择器添加 data-v-xxxx 属性选择器,实现作用域隔离

5️⃣ CSS Modules 支持

if (modules) {
  if (!options.isAsync) {
    throw new Error('modules 只能在异步编译中使用')
  }
  plugins.push(
    postcssModules({
      ...modulesOptions,
      getJSON: (_cssFileName, json) => { cssModules = json },
    }),
  )
}

⚙️ 这里引入了 postcss-modules 插件,并通过回调函数 getJSON 拿到模块名与类名映射表。
例如:

.btn { color: red; }

编译后:

{ "btn": "_btn_3f72b" }

6️⃣ PostCSS 处理与错误捕获

try {
  result = postcss(plugins).process(source, postCSSOptions)
  if (options.isAsync) {
    return result.then(result => ({
      code: result.css || '',
      map: result.map && result.map.toJSON(),
      errors,
      modules: cssModules,
      rawResult: result,
      dependencies: recordPlainCssDependencies(result.messages),
    }))
  }
  code = result.css
  outMap = result.map
} catch (e) {
  errors.push(e)
}

✳️ postcss.process() 返回一个 LazyResult 对象,可在同步与异步场景下灵活使用。
✳️ 所有插件执行后的中间产物与 SourceMap 均在此阶段生成。


五、拓展:如何自定义 PostCSS 流程

用户可以在调用时传入额外插件:

compileStyleAsync({
  source: '.title { color: red }',
  filename: 'App.vue',
  id: 'data-v-123',
  postcssPlugins: [
    require('autoprefixer'),
    require('cssnano')({ preset: 'default' }),
  ],
})

这允许你在 Vue SFC 编译过程中自动添加浏览器前缀压缩 CSS 输出,无须额外构建步骤。


六、潜在问题与注意事项

  1. 浏览器环境限制
    CSS Modules 不支持浏览器构建 (__GLOBAL__ / __ESM_BROWSER__ 分支会直接抛错)。
  2. SourceMap 错误映射
    若预处理器未正确生成 SourceMap,最终的映射文件可能出现偏移。
  3. 同步模式插件冲突
    某些 PostCSS 插件(如 cssnano)在同步模式下执行会导致异步任务未等待完成,应改用 compileStyleAsync()
  4. 依赖收集冗余
    Stylus 输出包含自身文件路径时需手动去重,因此源码中 dependencies.delete(filename)

七、结语

这段 compileStyle 代码堪称 Vue SFC 编译体系中最精妙的部分之一。它不仅连接了 Sass、PostCSS、CSS Modules 等多种生态,还通过函数式插件机制构建出一条高扩展性、可异步化的 CSS 处理管线。
理解它的运行逻辑,能帮助开发者在构建工具链时更好地自定义样式编译流程,例如实现原子化 CSS 注入动态主题切换等高级功能。


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

Vue SFC 编译全景总结:从源文件到运行时组件的完整链路

作者 excel
2025年11月8日 21:38

一、总览:Vue 编译器的多阶段模型

Vue SFC 编译过程可以分为三大层级:

[解析层]     parse()                →  把 .vue 源文件拆解为结构化描述 (SFCDescriptor)
[脚本编译层] compileScript()       →  将 <script> + <script setup> 转化为可执行逻辑
[模板编译层] compileTemplate()     →  将 <template> 转化为渲染函数 (render)

而我们讲解的七篇内容,全部集中在中间这一层:

📦 compileScript() = “脚本编译层”核心函数

它完成的任务是:
在编译阶段,把 <script setup> 转换为标准化的运行时组件定义。


二、主流程图(整体逻辑箭头)

下面这张逻辑箭头图,是 compileScript() 整个执行的主干流:

        用户 SFC 文件 (MyComp.vue)
                     │
                     ▼
          ┌────────────────────┐
          │ parse()            │
          │ → SFCDescriptor    │
          └────────────────────┘
                     │
                     ▼
          ┌───────────────────────────────┐
          │ compileScript(descriptor, opt) │
          └───────────────────────────────┘
                     │
   ┌─────────────────┼────────────────────────────────────────────────┐
   ▼                 ▼                                                ▼
[1]初始化上下文   [2]宏函数解析                           [3]绑定与作用域推断
 ScriptCompileCtx  defineProps / defineEmits ...         walkDeclaration() / BindingTypes
   │                 │                                                │
   ▼                 ▼                                                ▼
 记录 imports → 识别宏 → 注册 props/emits                识别 ref/reactive/const
   │                 │                                                │
   └──────────────┬──────────────────────────────────────────────────┘
                  ▼
        [4] 普通 <script> 与 <script setup> 合并
             ↓   - export default → const __default__
             ↓   - 代码重排(move)
             ↓
        [5] 模板内联与运行时选项生成
             ↓   genRuntimeProps() / genRuntimeEmits()
             ↓   compileTemplate() → render()
             ↓
        [6] 生成 defineComponent 包裹结构
             ↓   export default defineComponent({...})
             ↓
        [7] 生成 SourceMap 并返回 SFCScriptBlock

可以理解为:Vue 编译器先“读懂”开发者的语义(宏 + 绑定),
然后“改写”代码结构,最后“输出”运行时组件。


三、核心逻辑对应七个阶段(七篇内容回顾)

① 编译入口与上下文初始化

📘 功能:
创建 ScriptCompileContext,解析 AST,确定语言类型(JS/TS)。

📈 关键点:

const ctx = new ScriptCompileContext(sfc, options)

📍 结果:
得到一个带 MagicString 实例、userImportsbindingMetadata 的上下文对象。


② 宏函数解析机制

📘 功能:
识别并消解 definePropsdefineEmitsdefineExposedefineModel 等宏。

📈 核心逻辑:

if (processDefineProps(ctx, expr)) ...
else if (processDefineEmits(ctx, expr)) ...

📍 示例转换:

const props = defineProps<{ title: string }>()
↓ 编译后
props: { title: String }

宏是编译期静态语法,不在运行时代码中存在。


③ 绑定分析与作用域推断

📘 功能:
判断变量属于哪种绑定类型(refconstprops 等)。

📈 核心结构:

BindingTypes.SETUP_REF
BindingTypes.SETUP_CONST
BindingTypes.PROPS

📍 示例:

const a = ref(1)  → SETUP_REF
const b = 123     → LITERAL_CONST
let c = reactive({}) → SETUP_REACTIVE_CONST

④ 普通 <script><script setup> 的合并逻辑

📘 功能:
支持两种 script 共存,通过重写和移动合并为一个模块。

📈 关键代码:

// export default → const __default__ =
ctx.s.overwrite(start, end, `const __default__ = `)
ctx.s.move(scriptStartOffset!, scriptEndOffset!, 0)

📍 结果:

<script>export default { name: 'Comp' }</script>
<script setup>const msg = 'Hi'</script>const __default__ = { name: 'Comp' }
export default defineComponent({
  ...__default__,
  setup() { const msg = 'Hi'; return { msg } }
})

⑤ AST 遍历与声明解析

📘 功能:
深入扫描变量声明(含解构),识别响应式与常量。

📈 核心函数族:

walkDeclaration() → walkPattern() → walkObjectPattern() / walkArrayPattern()

📍 逻辑示例:

const { x, y } = reactive(state)
↓
registerBinding(x, SETUP_REACTIVE_CONST)
registerBinding(y, SETUP_REACTIVE_CONST)

⑥ 代码生成与 SourceMap 合并

📘 功能:
生成 props/emits/runtimeOptions;编译模板;生成 render 函数;
并将脚本与模板的 SourceMap 精确合并。

📈 关键调用链:

genRuntimeProps(ctx)
genRuntimeEmits(ctx)
compileTemplate({ inline: true })
mergeSourceMaps(scriptMap, templateMap, offset)

📍 效果:
模板被内联到 setup 函数中:

setup() {
  const msg = ref('Hello')
  return { msg }
},
render() { return h('div', msg.value) }

⑦ 最终导出与运行时结构

📘 功能:
拼装完整的 defineComponent() 调用,生成最终的导出代码。

📈 代码示例:

export default defineComponent({
  ...__default__,
  __name: 'MyComp',
  __isScriptSetup: true,
  props: { title: String },
  setup(__props, { expose }) {
    expose()
    const msg = ref('hi')
    return { msg }
  }
})

📍 注入属性:

字段 作用
__name 自动生成组件名
__isScriptSetup 标识 setup 模式组件
__ssrInlineRender 标记 SSR 内联渲染函数
__expose() 默认调用以闭合暴露域

四、逻辑回路图(从源码到运行时代码)

          MyComp.vue
              │
              ▼
        parse() → SFCDescriptor
              │
              ▼
      ┌──────────────────────────────┐
      │ compileScript(descriptor)    │
      └──────────────────────────────┘
              │
              ▼
     ┌──────────────────────────────────────┐
     │ 宏解析 defineProps / defineEmits ... │
     └──────────────────────────────────────┘
              │
              ▼
     ┌──────────────────────────────────────┐
     │ 变量绑定推断 BindingTypes            │
     └──────────────────────────────────────┘
              │
              ▼
     ┌──────────────────────────────────────┐
     │ 普通 script 与 setup 合并            │
     └──────────────────────────────────────┘
              │
              ▼
     ┌──────────────────────────────────────┐
     │ 生成 runtimeOptions (props, emits)   │
     └──────────────────────────────────────┘
              │
              ▼
     ┌──────────────────────────────────────┐
     │ 模板内联 compileTemplate             │
     └──────────────────────────────────────┘
              │
              ▼
     ┌──────────────────────────────────────┐
     │ defineComponent 封装 + SourceMap合并 │
     └──────────────────────────────────────┘
              │
              ▼
        ✅ 最终输出:JS 可执行组件模块

五、一个完整示例串联整个链路

📄 输入:

<script>
export default { name: 'Demo' }
</script>

<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{ msg: string }>()
const count = ref(0)
</script>

<template>
  <h1>{{ props.msg }} {{ count }}</h1>
</template>

📜 输出(编译后核心结构):

import { defineComponent as _defineComponent, ref as _ref, unref as _unref } from 'vue'

const __default__ = { name: 'Demo' }

export default /*#__PURE__*/_defineComponent({
  ...__default__,
  __name: 'Demo',
  props: { msg: String },
  setup(__props) {
    const count = _ref(0)
    return { props: __props, count }
  },
  render(_ctx) {
    return (_openBlock(), _createElementBlock("h1", null, _toDisplayString(_ctx.props.msg) + " " + _toDisplayString(_ctx.count), 1))
  }
})

👉 这就是 compileScript() + compileTemplate() 联合生成的最终产物。


六、潜在问题与优化空间

方向 潜在问题 Vue 团队的应对策略
宏函数滥用 宏只在编译时有效,错误使用可能混淆作用域 报错 + 提示用户改用 runtime API
性能 大文件 AST + SourceMap 合并耗时 使用 MagicString + lazy merge 提升性能
模板偏移 内联模板行号偏移 动态偏移修正(templateLineOffset
类型系统一致性 TS 泛型到 runtime props 的映射复杂 通过 ctx.propsTypeDecl 延迟生成 runtime 对象

七、总结:Vue 编译器的设计哲学

compileScript() 展现了 Vue 编译器设计的三大理念:

  1. 语法糖 → 编译期转换
    所有 <script setup> 特性都是编译期宏,无运行时负担。
  2. 静态分析 → 响应式自动化
    通过 AST 推断绑定类型,让模板访问自动展开 .value
  3. 渐进式架构 → 完全兼容旧语法
    <script><script setup> 可共存,保证平滑迁移。

八、结语

从这七个阶段的系统分析中,我们可以看出:
Vue 的 SFC 编译器并不是简单的“模板转函数”工具,而是一个完整的前端 DSL 编译系统

它将声明式语法 (<script setup>) 静态化为高效的 JavaScript 输出,
让开发者享受更简洁的语法同时保持运行时零成本。

一句话总结:
compileScript() 是把开发者“写的组件”转化为 Vue “理解的组件”的那道魔法之门。


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

Vue SFC 编译核心解析(第 7 篇)——最终组件导出与运行时代码结构

作者 excel
2025年11月8日 21:37

一、概念层:编译产物的目标

经过前面六个阶段(宏解析 → 绑定分析 → 合并逻辑 → AST 遍历 → 模板编译 → 代码生成),
Vue 编译器最终需要生成一个标准的运行时组件定义:

export default defineComponent({
  name: 'MyComp',
  props: { title: String },
  emits: ['submit'],
  setup(__props, { expose, emit }) {
    const msg = ref('hello')
    expose()
    return { msg }
  }
})

这就是编译器的目标产物,它必须满足三个条件:

  1. 语义完整:包含所有声明的 props / emits / expose / setup 内容。
  2. 运行时可执行:直接可被浏览器或 bundler 执行。
  3. 调试可追踪:保持 SourceMap 与原始 .vue 文件一致。

二、原理层:defineComponent() 包裹生成逻辑

编译器在最后阶段会将 setup 内容包裹到 defineComponent() 调用中。
核心逻辑如下:

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 模式允许类型系统理解 propsemits
JavaScript 模式则以运行时结构为主,更简洁。


四、实践层:运行时辅助选项注入

在生成组件定义对象时,Vue 会注入多个隐藏的运行时属性:

1️⃣ __name

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

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

if (hasInlinedSsrRenderFn) {
  runtimeOptions += `\n  __ssrInlineRender: true,`
}

作用:

  • 在 SSR 模式下标记模板已被内联;
  • 避免重复加载 render 函数;
  • 优化服务端渲染的性能与缓存逻辑。

4️⃣ __expose()

const exposeCall = ctx.hasDefineExposeCall || options.inlineTemplate ? '' : '  __expose();\n'

作用:

  • 若用户未显式调用 defineExpose(),编译器自动插入默认调用;
  • 确保 <script setup> 默认闭合暴露
  • 避免无意间将局部变量泄漏到实例。

五、拓展层:返回值封装与响应式展开

在 setup 函数结束前,Vue 编译器会注入:

return { ...setupBindings, ...scriptBindings }

如果检测到变量可能是 ref,则在模板层编译时自动展开 .value

这一阶段结合了前文的 BindingTypes 结果,实现了“静态响应式展开”优化:

模板渲染时无需动态判定类型,而是直接生成正确的访问方式。


六、源文件映射与最终结构

生成的最终文件结构大致如下:

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 辅助生成,并由作者整理审核。

提升页面质感:CSS 重复格子背景的实用技巧

作者 elvinnn
2025年11月8日 21:36

重复格子背景

最近发现不少开发类的网站都在首页使用上了重复格子的背景,例如下图中左侧以 relay.app 为例子的截图。

这种背景主要是为了在现在大量留白的设计风格下,避免页面过于空旷,增加一些视觉上的丰富性,不然就会如下图中右侧的截图那样在去掉背景后略显单调。

背景有无效果对比

接下来具体讲一讲如何用 CSS 实现这种重复格子背景,主要有两种方法:

  • 局部格子图片 + 背景重复
  • CSS 渐变 + 背景重复(推荐)

方法一:局部格子图片 + 背景重复

relay.app 使用的就是这种方法,这应该是最常见也是最容易想到的方法,就是先准备一张格子图片,例如:

格子图片

然后通过 background 相关属性进行设置即可,关键在于:

  • background-repeat 需要设置为 repeat,保证图片的重复。
  • background-size 可用于调整背景中格子的密度。
background-image: url(https://framerusercontent.com/images/9c47fOR3CNoSsEtr6IEYJoKM.svg?width=126&height=126);
background-size: 63px auto;
background-repeat: repeat;
background-position: left top;

下面是一个前端组件使用后的效果示例 (访问 我的博客 可以进行交互式体验):

SVG 图片实现效果

方法二:CSS 渐变 + 背景重复(推荐)

我们还可以通过 CSS 渐变来替代方法一中依赖的外部图片资源,从而实现更快的加载和更灵活的控制,一个简单的例子如下:

background-image:
    linear-gradient(to right, rgba(59, 130, 246, 0.08) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(59, 130, 246, 0.08) 1px, transparent 1px);
background-size: 40px 40px;
transform: rotate(-3deg) scale(1.2);

代码的关键在于:

  • 通过 linear-gradient 创建一个只有1px宽度的渐变条,颜色为 rgba(59, 130, 246, 0.08) 的部分占 1px,其余部分透明,to right 控制竖向的线条,to bottom 控制横向的线条。
  • 通过 background-size 控制格子的大小。
  • 通过 transform 可以制造倾斜的效果,更加有趣一些。

下面是一个前端组件使用后的效果示例 (访问 我的博客 可以进行交互式体验):

CSS 渐变实现效果

我最近开发的 JTool.dev 首页就是通过方法二实现的效果,也欢迎大家体验:

JTool.dev 首页截图

Vue SFC 编译核心解析(第 6 篇)——代码生成与 SourceMap 合并:从编译结果到调试追踪

作者 excel
2025年11月8日 21:34

一、概念层:代码生成的本质

compileScript() 的最终目标是输出一个完整可执行的 JavaScript 模块
包括:

  • 组件定义 (export default defineComponent({...}))
  • props / emits / expose 等运行时属性
  • setup() 函数体
  • 模板(可能内联或独立编译)
  • CSS 变量与辅助函数导入

这一阶段被称为 Code Generation(代码生成阶段)
在 Vue 编译器中,它不仅生成代码字符串,还同时维护:

  1. 精确的字符位置信息
  2. 跨模板与脚本的 SourceMap 映射
  3. 辅助函数导入关系

二、原理层:生成阶段主要流程

compileScript() 的后半部分,我们可以看到完整的代码生成顺序:

// 1. 生成 runtime props
const propsDecl = genRuntimeProps(ctx)

// 2. 生成 runtime emits
const emitsDecl = genRuntimeEmits(ctx)

// 3. 内联模板(可选)
const { code, map } = compileTemplate(...)

// 4. 生成 defineComponent 包裹体
ctx.s.prependLeft(
  startOffset,
  `${genDefaultAs} defineComponent({... setup() {...}})`
)

// 5. 合并 SourceMap
map = mergeSourceMaps(scriptMap, templateMap, templateLineOffset)

简而言之:

Props / Emits 生成
     ↓
模板代码内联
     ↓
defineComponent 封装
     ↓
SourceMap 合并

三、对比层:不同模式的生成策略

模式 特征 模板处理 典型用途
非内联模式 (inlineTemplate: false) 默认模式,模板单独编译 模板由 compileTemplate() 在单独阶段处理 开发模式(支持热重载)
内联模式 (inlineTemplate: true) 将模板编译结果直接内嵌进 setup() 模板在同一代码生成阶段完成 生产模式(减少 I/O 与缓存层)

四、实践层:Props 与 Emits 的运行时代码生成

1️⃣ 生成 Props

函数:genRuntimeProps(ctx)

输入:
宏调用结果(由 processDefineProps 收集)

输出示例:

props: {
  title: String,
  count: Number,
}

核心代码:

const propsDecl = genRuntimeProps(ctx)
if (propsDecl) runtimeOptions += `\n  props: ${propsDecl},`

💡 ctx.propsTypeDecl 可能来自 TypeScript 泛型声明,
编译器会在这里将类型信息转化为运行时对象结构。


2️⃣ 生成 Emits

函数:genRuntimeEmits(ctx)

输入:
宏调用 defineEmits() 的结果

输出示例:

emits: ['update', 'submit']

或带验证:

emits: {
  submit: payload => payload.id !== undefined
}

核心调用:

const emitsDecl = genRuntimeEmits(ctx)
if (emitsDecl) runtimeOptions += `\n  emits: ${emitsDecl},`

3️⃣ 模板内联生成

内联模板时,编译器会直接调用:

const { code, ast, preamble, map } = compileTemplate({
  filename,
  source: sfc.template.content,
  id: scopeId,
  isProd: options.isProd,
  ssrCssVars: sfc.cssVars,
  compilerOptions: { inline: true, bindingMetadata: ctx.bindingMetadata },
})

关键点:

  • inline: true 表示直接生成可内嵌的 render() 函数;
  • bindingMetadata 告诉模板编译器变量的绑定类型;
  • preamble 可能包含自动导入的 helper 函数;
  • map 是模板部分的 SourceMap(用于后续合并)。

输出示例:

function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", null, "Hello " + _toDisplayString(_ctx.msg), 1))
}

五、拓展层:辅助函数导入(helperImports)

在生成代码时,编译器还会动态收集所有运行时依赖,例如:

ctx.helperImports.add('defineComponent')
ctx.helperImports.add('ref')
ctx.helperImports.add('unref')

最后统一导入:

import { defineComponent as _defineComponent, ref as _ref, unref as _unref } from 'vue'

对应源码:

if (ctx.helperImports.size > 0) {
  ctx.s.prepend(
    `import { ${[...ctx.helperImports].map(h => `${h} as _${h}`).join(', ')} } from 'vue'\n`
  )
}

这种方式确保生成代码最简化,不重复导入。


六、深入:SourceMap 合并原理 (mergeSourceMaps)

模板和脚本在不同阶段生成各自的 SourceMap,
Vue 通过 mergeSourceMaps() 将它们精确叠加。

核心逻辑:

export function mergeSourceMaps(scriptMap, templateMap, templateLineOffset) {
  const generator = new SourceMapGenerator()

  const addMapping = (map, lineOffset = 0) => {
    const consumer = new SourceMapConsumer(map)
    consumer.eachMapping(m => {
      if (m.originalLine == null) return
      generator.addMapping({
        generated: { line: m.generatedLine + lineOffset, column: m.generatedColumn },
        original: { line: m.originalLine, column: m.originalColumn! },
        source: m.source,
        name: m.name,
      })
    })
  }

  addMapping(scriptMap)
  addMapping(templateMap, templateLineOffset)
  return generator.toJSON()
}

逐步解释:

  1. 创建新的 SourceMapGenerator()

  2. 遍历两个 SourceMap:

    • scriptMap(脚本源);
    • templateMap(模板源);
  3. 对模板映射应用行偏移(templateLineOffset),
    让模板生成的行号与脚本插入位置对齐;

  4. 合并所有映射条目;

  5. 输出最终 JSON。

这样,在浏览器调试 .vue 文件时:

即使模板被内联进 JS,也能精确跳回源 .vue 文件位置。


七、潜在问题与工程考量

问题 说明
内联模板 SourceMap 偏移 若模板中包含多行注释或 preamble,行号计算可能略偏。
高频 AST 操作性能 大量 SourceMap 合并在大型组件中可能导致构建变慢。
第三方工具兼容性 Rollup / Vite 插件需正确消费合并后的 SourceMap。
CSS 变量与 helper 冲突 CSS 变量注入也可能引入额外导入(unref),需去重。

Vue 在此使用了缓存 + 懒生成策略(lazy generation)来平衡性能与精度。


八、小结

这一篇我们分析了 Vue 在代码生成阶段的四大核心机制:

  1. Props / Emits 的运行时生成
  2. 模板内联与 compileTemplate 调用
  3. 辅助函数自动导入系统
  4. SourceMap 的行级合并算法

这些机制共同构成了 Vue SFC 编译的“输出层”,
.vue 文件既能在构建时最小化,又能在调试时完整溯源。


在下一篇(第 7 篇),我们将进入“输出收尾阶段”:
🔧 《最终组件导出与运行时代码结构》
解析最终生成的 export default defineComponent({...}) 代码结构,以及各辅助字段(__isScriptSetup__ssrInlineRender__name 等)的作用。


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

Vue SFC 编译核心解析(第 5 篇)——AST 遍历与声明解析:walkDeclaration 系列函数详解

作者 excel
2025年11月8日 21:30

一、概念层:AST 遍历在 Vue 编译中的地位

compileScript() 中,编译器需要识别以下几类变量声明:

const count = ref(0)
let name = 'Vue'
const { a, b } = reactive(obj)

Vue 必须在编译阶段理解这些声明,以判断:

  • 哪些变量是响应式(ref / reactive);
  • 哪些是常量;
  • 哪些是 props;
  • 哪些只是普通局部变量。

这一分析的关键函数就是:

walkDeclaration()

它负责递归遍历 AST 节点,推断每个绑定的类型,并将结果注册进 bindings 映射表中。


二、原理层:walkDeclaration 的核心逻辑

原函数签名如下:

function walkDeclaration(
  from: 'script' | 'scriptSetup',
  node: Declaration,
  bindings: Record<string, BindingTypes>,
  userImportAliases: Record<string, string>,
  hoistStatic: boolean,
  isPropsDestructureEnabled = false,
): boolean

参数说明:

参数 含义
from 当前声明来源(普通脚本或 setup 脚本)
node 当前 AST 声明节点(VariableDeclaration / FunctionDeclaration 等)
bindings 存储当前作用域的变量绑定类型
userImportAliases 用于识别 ref / reactive 的真实导入名
hoistStatic 是否允许静态常量上移
isPropsDestructureEnabled 是否启用 props 解构优化

函数整体结构:

if (node.type === 'VariableDeclaration') {
  // 1. 遍历变量声明
} else if (node.type === 'TSEnumDeclaration') {
  // 2. TypeScript 枚举声明
} else if (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') {
  // 3. 函数与类声明
}

输出:返回一个布尔值 isAllLiteral,用于判断该声明是否为纯字面量常量。


三、对比层:walkDeclarationanalyzeScriptBindings 的区别

函数 工作层级 分析内容 用途
analyzeScriptBindings 顶层 快速扫描 script 区域的导出绑定 普通脚本变量分析
walkDeclaration 递归层 深入遍历 setup 变量结构 组合式变量分析

前者是全局扫描(只看顶层变量),
后者是递归遍历模式结构(处理解构、数组、对象、函数等复杂绑定)。


四、实践层:walkDeclaration 主体流程详解

1️⃣ 处理普通变量声明

if (node.type === 'VariableDeclaration') {
  const isConst = node.kind === 'const'
  isAllLiteral =
    isConst &&
    node.declarations.every(
      decl => decl.id.type === 'Identifier' && isStaticNode(decl.init!),
    )

  for (const { id, init: _init } of node.declarations) {
    const init = _init && unwrapTSNode(_init)
    if (id.type === 'Identifier') {
      let bindingType

      if (isConst && isStaticNode(init!)) {
        bindingType = BindingTypes.LITERAL_CONST
      } else if (isCallOf(init, userImportAliases['reactive'])) {
        bindingType = BindingTypes.SETUP_REACTIVE_CONST
      } else if (isCallOf(init, userImportAliases['ref'])) {
        bindingType = BindingTypes.SETUP_REF
      } else if (isConst) {
        bindingType = BindingTypes.SETUP_MAYBE_REF
      } else {
        bindingType = BindingTypes.SETUP_LET
      }

      registerBinding(bindings, id, bindingType)
    }
  }
}

🧩 步骤拆解:

步骤 操作 说明
判断 const / let 影响变量是否可变
调用 isStaticNode(init) 判断右值是否为纯字面量(例如数字、字符串)
调用 isCallOf(init, userImportAliases['ref']) 检测是否调用了 ref()reactive()
确定绑定类型 通过逻辑树推断对应的 BindingTypes
注册绑定 调用 registerBinding(bindings, id, bindingType) 存入记录表

2️⃣ 处理解构模式:walkObjectPatternwalkArrayPattern

Vue 的 setup 可能出现这种写法:

const { x, y } = usePoint()
const [a, b] = list

编译器需要递归解析这些模式结构。

对象模式:

function walkObjectPattern(node, bindings, isConst, isDefineCall) {
  for (const p of node.properties) {
    if (p.type === 'ObjectProperty') {
      if (p.key === p.value) {
        registerBinding(bindings, p.key, BindingTypes.SETUP_MAYBE_REF)
      } else {
        walkPattern(p.value, bindings, isConst, isDefineCall)
      }
    } else {
      // ...rest 参数
      registerBinding(bindings, p.argument, BindingTypes.SETUP_LET)
    }
  }
}

数组模式:

function walkArrayPattern(node, bindings, isConst, isDefineCall) {
  for (const e of node.elements) {
    if (e) walkPattern(e, bindings, isConst, isDefineCall)
  }
}

3️⃣ 递归统一入口:walkPattern

function walkPattern(node, bindings, isConst, isDefineCall = false) {
  if (node.type === 'Identifier') {
    registerBinding(bindings, node, isConst ? BindingTypes.SETUP_MAYBE_REF : BindingTypes.SETUP_LET)
  } else if (node.type === 'ObjectPattern') {
    walkObjectPattern(node, bindings, isConst)
  } else if (node.type === 'ArrayPattern') {
    walkArrayPattern(node, bindings, isConst)
  } else if (node.type === 'AssignmentPattern') {
    walkPattern(node.left, bindings, isConst)
  }
}

关键点说明:

  • 递归调用处理任意层级解构;
  • 对默认值模式(AssignmentPattern)同样递归;
  • 所有最终标识符都会落入 registerBinding()

五、拓展层:静态节点检测与不可变优化

isStaticNode()walkDeclaration 的辅助函数,用于检测一个表达式是否是纯常量

function isStaticNode(node: Node): boolean {
  switch (node.type) {
    case 'StringLiteral':
    case 'NumericLiteral':
    case 'BooleanLiteral':
    case 'NullLiteral':
    case 'BigIntLiteral':
      return true
    case 'UnaryExpression':
      return isStaticNode(node.argument)
    case 'BinaryExpression':
      return isStaticNode(node.left) && isStaticNode(node.right)
    case 'ConditionalExpression':
      return (
        isStaticNode(node.test) &&
        isStaticNode(node.consequent) &&
        isStaticNode(node.alternate)
      )
  }
  return false
}

这样,像下面的声明:

const a = 100
const b = 1 + 2

都会被标记为 BindingTypes.LITERAL_CONST
从而在渲染函数中跳过响应式追踪,提升性能。


六、潜在问题与性能考量

问题 说明
复杂模式递归性能 深层嵌套解构可能造成递归栈压力(Vue 内部已优化为迭代式遍历)。
TS 类型节点干扰 TypeScript AST 节点会被包裹,需要通过 unwrapTSNode() 去除。
动态函数检测局限 若函数名被重命名(如 const r = ref),则无法识别。
宏嵌套干扰 宏(如 defineProps)中的变量会被另行处理,不能直接分析。

七、小结

通过本篇我们理解了:

  • walkDeclaration 如何识别各种声明;
  • Vue 编译器如何用静态语义推断响应式特征;
  • walkObjectPattern / walkArrayPattern / walkPattern 如何递归遍历结构;
  • isStaticNode 如何识别字面量常量。

walkDeclaration 是整个 <script setup> 编译器的“语义扫描仪”,
它使 Vue 能在模板编译阶段就确定哪些变量需要响应式展开,哪些可以静态化。


下一篇(第 6 篇)我们将进入代码生成阶段,
📦 《代码生成与 SourceMap 合并:从编译结果到调试追踪》
分析 genRuntimeProps()genRuntimeEmits() 以及 mergeSourceMaps() 如何将模板与脚本拼接为最终可执行代码。


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

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

作者 excel
2025年11月8日 21:28

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

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

<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() 的任务就是将这两部分整合成一个完整的、合法的运行时代码:

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

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

compileScript() 中这一逻辑的核心是 “默认导出重写” + “代码块移动” + “作用域合并”
换句话说,它不会简单地“拼接两个字符串”,而是通过 AST 分析与 MagicString 操作完成精确重构。

整体算法思路如下:

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. 检测导出与重写逻辑

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. 命名导出转换为导入或赋值

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> 之后,
编译器需要调整顺序,使普通脚本块先出现。

if (scriptStartOffset! > startOffset) {
  ctx.s.move(scriptStartOffset!, scriptEndOffset!, 0)
}

含义:

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

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

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

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 辅助生成,并由作者整理审核。

❌
❌