Skip to content

7.2 SFC 编译:<script setup> 与 CSS v-bind 是如何工作的?

Vue 3 的单文件组件(SFC)编译系统是一个复杂而精妙的工程,它将 .vue 文件转换为浏览器可执行的 JavaScript 代码。本节将深入分析 <script setup> 语法糖和 CSS v-bind 功能的编译原理,揭示 Vue 3 如何在编译时实现这些强大特性。

SFC 编译架构概览

编译流程

Vue 3 的 SFC 编译过程主要由 @vue/compiler-sfc 包负责,核心文件包括:

  • compileScript.ts - 处理 <script><script setup> 块的编译
  • compileStyle.ts - 处理 <style> 块的编译,包括作用域样式和 CSS v-bind
  • compileTemplate.ts - 处理 <template> 块的编译
typescript
// 编译选项接口
export interface SFCScriptCompileOptions {
  id: string                    // 组件唯一标识
  isProd?: boolean             // 生产模式标志
  sourceMap?: boolean          // 源码映射
  babelParserPlugins?: ParserPlugin[]  // Babel 解析插件
  inlineTemplate?: boolean     // 内联模板
  hoistStatic?: boolean        // 静态提升
  propsDestructure?: boolean | 'error'  // Props 解构
}

编译上下文

编译过程中,Vue 使用 ScriptCompileContext 来维护编译状态:

typescript
class ScriptCompileContext {
  bindingMetadata: BindingMetadata  // 绑定元数据
  userImports: Record<string, ImportBinding>  // 用户导入
  helperImports: Set<string>       // 辅助函数导入
  hasDefinePropsCall: boolean      // 是否有 defineProps 调用
  hasDefineEmitCall: boolean       // 是否有 defineEmits 调用
  // ... 其他状态
}

<script setup> 语法糖编译原理

编译转换流程

<script setup> 的编译过程可以分为以下几个阶段:

1. AST 解析与预处理

typescript
// compileScript.ts 核心逻辑
export function compileScript(
  sfc: SFCDescriptor,
  options: SFCScriptCompileOptions,
): SFCScriptBlock {
  const { script, scriptSetup } = sfc
  const ctx = new ScriptCompileContext(sfc, options)
  
  // 解析 `<script setup>` 的 AST
  const scriptSetupAst = ctx.scriptSetupAst!
  
  // 处理导入声明
  for (const node of scriptSetupAst.body) {
    if (node.type === 'ImportDeclaration') {
      hoistNode(node)  // 提升导入到顶部
      // 处理宏导入的去重和移除
    }
  }
}

2. 宏处理与转换

<script setup> 中的编译时宏会被特殊处理:

typescript
// 宏列表
const MACROS = [
  'defineProps',
  'defineEmits', 
  'defineExpose',
  'defineOptions',
  'defineSlots',
  'defineModel',
  'withDefaults'
]

// 处理表达式语句中的宏调用
for (const node of scriptSetupAst.body) {
  if (node.type === 'ExpressionStatement') {
    const expr = unwrapTSNode(node.expression)
    
    if (
      processDefineProps(ctx, expr) ||
      processDefineEmits(ctx, expr) ||
      processDefineOptions(ctx, expr) ||
      processDefineSlots(ctx, expr)
    ) {
      // 移除宏调用语句
      ctx.s.remove(node.start! + startOffset, node.end! + startOffset)
    }
  }
}

3. setup 函数生成

最终,<script setup> 的内容会被包装成一个 setup 函数:

typescript
// 生成 setup 函数包装
const genDefaultAs = options.genDefaultAs
  ? `const ${options.genDefaultAs} =`
  : `export default`

// TypeScript 模式
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 {
  // JavaScript 模式使用 Object.assign
  ctx.s.prependLeft(
    startOffset,
    `\n${genDefaultAs} {${runtimeOptions}\n  ` +
      `${hasAwait ? `async ` : ``}setup(${args}) {\n${exposeCall}`,
  )
  ctx.s.appendRight(endOffset, `}`)
}

defineProps 编译机制

运行时声明处理

typescript
// defineProps.ts
export function processDefineProps(
  ctx: ScriptCompileContext,
  node: Node,
  declId?: LVal,
): boolean {
  if (!isCallOf(node, DEFINE_PROPS)) return false
  
  ctx.hasDefinePropsCall = true
  ctx.propsRuntimeDecl = node.arguments[0]
  
  // 注册绑定
  if (ctx.propsRuntimeDecl) {
    for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
      if (!(key in ctx.bindingMetadata)) {
        ctx.bindingMetadata[key] = BindingTypes.PROPS
      }
    }
  }
  
  // 类型参数处理
  if (node.typeParameters) {
    ctx.propsTypeDecl = node.typeParameters.params[0]
  }
  
  return true
}

Props 解构支持

Vue 3.5+ 支持 Props 解构,编译器会进行特殊处理:

typescript
// 解构转换示例
// 源码:const { foo, bar = 'default' } = defineProps<{foo: string, bar?: string}>()
// 编译后:
const __props = defineProps<{foo: string, bar?: string}>()
const foo = toRef(__props, 'foo')
const bar = toRef(__props, 'bar', 'default')

defineEmits 编译机制

typescript
// defineEmits.ts
export function processDefineEmits(
  ctx: ScriptCompileContext,
  node: Node,
  declId?: LVal,
): boolean {
  if (!isCallOf(node, DEFINE_EMITS)) return false
  
  ctx.hasDefineEmitCall = true
  ctx.emitsRuntimeDecl = node.arguments[0]
  
  // 类型声明处理
  if (node.typeParameters) {
    ctx.emitsTypeDecl = node.typeParameters.params[0]
  }
  
  return true
}

// 运行时 emits 生成
export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
  let emitsDecl = ''
  
  if (ctx.emitsRuntimeDecl) {
    emitsDecl = ctx.getString(ctx.emitsRuntimeDecl).trim()
  } else if (ctx.emitsTypeDecl) {
    const typeDeclaredEmits = extractRuntimeEmits(ctx)
    emitsDecl = typeDeclaredEmits.size
      ? `[${Array.from(typeDeclaredEmits)
          .map(k => JSON.stringify(k))
          .join(', ')}]`
      : ``
  }
  
  return emitsDecl
}

CSS v-bind 响应式变量实现

CSS 变量解析

CSS v-bind 功能通过 PostCSS 插件实现,核心逻辑在 cssVars.ts 中:

typescript
// 解析 CSS 中的 v-bind 表达式
const vBindRE = /v-bind\s*\(/g

export function parseCssVars(sfc: SFCDescriptor): string[] {
  const vars: string[] = []
  
  sfc.styles.forEach(style => {
    let match
    // 移除注释
    const content = style.content.replace(/\/\*([\s\S]*?)\*\/|\/\/.*/g, '')
    
    while ((match = vBindRE.exec(content))) {
      const start = match.index + match[0].length
      const end = lexBinding(content, start)
      
      if (end !== null) {
        const variable = normalizeExpression(content.slice(start, end))
        if (!vars.includes(variable)) {
          vars.push(variable)
        }
      }
    }
  })
  
  return vars
}

词法分析器

为了正确解析 v-bind 表达式,Vue 实现了一个简单的词法分析器:

typescript
enum LexerState {
  inParens,
  inSingleQuoteString,
  inDoubleQuoteString,
}

function lexBinding(content: string, start: number): number | null {
  let state: LexerState = LexerState.inParens
  let parenDepth = 0

  for (let i = start; i < content.length; i++) {
    const char = content.charAt(i)
    
    switch (state) {
      case LexerState.inParens:
        if (char === `'`) {
          state = LexerState.inSingleQuoteString
        } else if (char === `"`) {
          state = LexerState.inDoubleQuoteString
        } else if (char === `(`) {
          parenDepth++
        } else if (char === `)`) {
          if (parenDepth > 0) {
            parenDepth--
          } else {
            return i  // 找到匹配的右括号
          }
        }
        break
      // ... 其他状态处理
    }
  }
  
  return null
}

CSS 变量转换

PostCSS 插件负责将 v-bind 表达式转换为 CSS 自定义属性:

typescript
// cssVars.ts - PostCSS 插件
export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
  const { id, isProd } = opts!
  
  return {
    postcssPlugin: 'vue-sfc-vars',
    Declaration(decl) {
      const value = decl.value
      
      if (vBindRE.test(value)) {
        vBindRE.lastIndex = 0
        let transformed = ''
        let lastIndex = 0
        let match
        
        while ((match = vBindRE.exec(value))) {
          const start = match.index + match[0].length
          const end = lexBinding(value, start)
          
          if (end !== null) {
            const variable = normalizeExpression(value.slice(start, end))
            transformed +=
              value.slice(lastIndex, match.index) +
              `var(--${genVarName(id, variable, isProd)})`
            lastIndex = end + 1
          }
        }
        
        decl.value = transformed + value.slice(lastIndex)
      }
    },
  }
}

变量名生成

typescript
function genVarName(
  id: string,
  raw: string,
  isProd: boolean,
  isSSR = false,
): string {
  if (isProd) {
    return hash(id + raw)  // 生产模式使用哈希
  } else {
    // 开发模式使用可读名称
    return `${id}-${getEscapedCssVarName(raw, isSSR)}`
  }
}

运行时代码生成

编译器会生成运行时代码来更新 CSS 变量:

typescript
// 生成 CSS 变量更新代码
export function genCssVarsCode(
  vars: string[],
  bindings: BindingMetadata,
  id: string,
  isProd: boolean,
) {
  const varsExp = genCssVarsFromList(vars, id, isProd)
  const exp = createSimpleExpression(varsExp, false)
  
  const context = createTransformContext(createRoot([]), {
    prefixIdentifiers: true,
    inline: true,
    bindingMetadata: bindings.__isScriptSetup === false ? undefined : bindings,
  })
  
  const transformed = processExpression(exp, context)
  
  return `_${CSS_VARS_HELPER}(_ctx => (${transformedString}))`
}

// 生成变量对象
export function genCssVarsFromList(
  vars: string[],
  id: string,
  isProd: boolean,
  isSSR = false,
): string {
  return `{\n  ${vars
    .map(
      key =>
        `"${isSSR ? `:--` : ``}${genVarName(id, key, isProd, isSSR)}": (${key})`,
    )
    .join(',\n  ')}\n}`
}

作用域样式(Scoped CSS)实现

选择器转换原理

作用域样式通过 PostCSS 插件 pluginScoped.ts 实现:

typescript
const scopedPlugin: PluginCreator<string> = (id = '') => {
  const keyframes = Object.create(null)
  const shortId = id.replace(/^data-v-/, '')

  return {
    postcssPlugin: 'vue-sfc-scoped',
    Rule(rule) {
      processRule(id, rule)  // 处理规则
    },
    AtRule(node) {
      // 处理 @keyframes
      if (keyframesRE.test(node.name) && !node.params.endsWith(`-${shortId}`)) {
        keyframes[node.params] = node.params = node.params + '-' + shortId
      }
    },
  }
}

选择器重写逻辑

typescript
function rewriteSelector(
  id: string,
  rule: Rule,
  selector: selectorParser.Selector,
  selectorRoot: selectorParser.Root,
  deep: boolean,
  slotted = false,
) {
  let node: selectorParser.Node | null = null
  let shouldInject = !deep
  
  selector.each(n => {
    // 处理 :deep() 伪类
    if (n.type === 'pseudo' && (n.value === ':deep' || n.value === '::v-deep')) {
      ;(rule as any).__deep = true
      // 重写选择器逻辑
      return false
    }
    
    // 处理 :slotted() 伪类
    if (n.value === ':slotted' || n.value === '::v-slotted') {
      rewriteSelector(id, rule, n.nodes[0], selectorRoot, deep, true)
      shouldInject = false
      return false
    }
    
    // 处理 :global() 伪类
    if (n.value === ':global' || n.value === '::v-global') {
      selector.replaceWith(n.nodes[0])
      return false
    }
  })
  
  // 注入属性选择器
  if (shouldInject) {
    const idToAdd = slotted ? id + '-s' : id
    selector.insertAfter(
      node as any,
      selectorParser.attribute({
        attribute: idToAdd,
        value: idToAdd,
        raws: {},
        quoteMark: `"`,
      }),
    )
  }
}

特殊选择器处理

:deep() 深度选择器

css
/* 源码 */
.foo :deep(.bar) {
  color: red;
}

/* 编译后 */
.foo[data-v-123] .bar {
  color: red;
}

:slotted() 插槽选择器

css
/* 源码 */
:slotted(.item) {
  color: blue;
}

/* 编译后 */
.item[data-v-123-s] {
  color: blue;
}

:global() 全局选择器

css
/* 源码 */
:global(.global-class) {
  margin: 0;
}

/* 编译后 */
.global-class {
  margin: 0;
}

CSS Modules 支持

模块化配置

typescript
export interface CSSModulesOptions {
  scopeBehaviour?: 'global' | 'local'
  generateScopedName?: string | ((name: string, filename: string, css: string) => string)
  hashPrefix?: string
  localsConvention?: 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly'
  exportGlobals?: boolean
  globalModulePaths?: RegExp[]
}

编译集成

typescript
// compileStyle.ts
if (modules) {
  plugins.push(
    postcssModules({
      ...modulesOptions,
      getJSON: (_cssFileName: string, json: Record<string, string>) => {
        cssModules = json  // 收集类名映射
      },
    }),
  )
}

编译优化策略

静态提升

typescript
// 静态常量提升
function isStaticNode(node: Node): boolean {
  node = unwrapTSNode(node)

  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)
    // ... 其他静态节点判断
  }
  
  return false
}

类型推导优化

typescript
// TypeScript 集成
if (ctx.isTS) {
  // 使用 defineComponent 包装以保持类型信息
  ctx.s.prependLeft(
    startOffset,
    `\n${genDefaultAs} /*@__PURE__*/${ctx.helper(
      `defineComponent`,
    )}({${def}${runtimeOptions}\n  setup(${args}) {\n`,
  )
}

热模块替换(HMR)

typescript
// HMR 支持
if (__DEV__) {
  // 注入 HMR 相关代码
  ctx.s.append(`\n__hmrId = "${id}"\n`)
  ctx.s.append(`typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.updateComponent(__hmrId, __component__)\n`)
}

性能优化与最佳实践

编译时优化

  1. 宏调用移除:编译时宏在运行时完全消失
  2. 静态分析:编译器进行静态分析以优化绑定类型
  3. 代码分割:支持异步组件和动态导入
  4. Tree Shaking:移除未使用的导入和代码

开发体验优化

  1. 源码映射:保持调试时的源码对应关系
  2. 错误提示:编译时提供详细的错误信息
  3. 类型检查:TypeScript 集成提供类型安全

生产环境优化

  1. 变量名混淆:生产模式下使用哈希变量名
  2. 代码压缩:移除开发时的辅助代码
  3. 缓存优化:基于内容哈希的缓存策略

总结

Vue 3 的 SFC 编译系统展现了现代前端工程化的精髓:

  1. 编译时优化:通过编译时转换实现零运行时开销的语法糖
  2. 类型安全:深度集成 TypeScript,提供完整的类型推导
  3. 开发体验:提供直观的 API 和详细的错误提示
  4. 性能优化:通过静态分析和代码生成实现最优性能
  5. 生态兼容:与现有工具链无缝集成

<script setup> 和 CSS v-bind 的实现充分体现了 Vue 3 "编译时优化,运行时高效" 的设计哲学,为开发者提供了既强大又易用的开发体验。这种设计不仅提升了开发效率,也为 Vue 3 在性能和可维护性方面奠定了坚实基础。


微信公众号二维码