Appearance
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-bindcompileTemplate.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`)
}性能优化与最佳实践
编译时优化
- 宏调用移除:编译时宏在运行时完全消失
- 静态分析:编译器进行静态分析以优化绑定类型
- 代码分割:支持异步组件和动态导入
- Tree Shaking:移除未使用的导入和代码
开发体验优化
- 源码映射:保持调试时的源码对应关系
- 错误提示:编译时提供详细的错误信息
- 类型检查:TypeScript 集成提供类型安全
生产环境优化
- 变量名混淆:生产模式下使用哈希变量名
- 代码压缩:移除开发时的辅助代码
- 缓存优化:基于内容哈希的缓存策略
总结
Vue 3 的 SFC 编译系统展现了现代前端工程化的精髓:
- 编译时优化:通过编译时转换实现零运行时开销的语法糖
- 类型安全:深度集成 TypeScript,提供完整的类型推导
- 开发体验:提供直观的 API 和详细的错误提示
- 性能优化:通过静态分析和代码生成实现最优性能
- 生态兼容:与现有工具链无缝集成
<script setup> 和 CSS v-bind 的实现充分体现了 Vue 3 "编译时优化,运行时高效" 的设计哲学,为开发者提供了既强大又易用的开发体验。这种设计不仅提升了开发效率,也为 Vue 3 在性能和可维护性方面奠定了坚实基础。
