Appearance
:SFC 编译:<script setup> 与 CSS v-bind 是如何工作的?**
引言
一个 .vue 文件(SFC - 单文件组件)本身并不能在浏览器中运行。它是一种“高级语言”,必须由 @vue/compiler-sfc 包进行编译,将其“翻译”成标准的 JavaScript 和 CSS。
这个“翻译”过程,让 Vue 3 实现了两个最强大的“语法糖”:<script setup> 和 <style> 中的 v-bind()。本节将深入探究编译器是如何实现这两个“魔法”的。
<script setup>:自动封装的 setup
<script setup> 的目标是消除组合式 API 的“样板代码”(setup() { return { ... } })。它让你写的代码“看起来”像是在模块的顶层,但编译器在背后做了大量工作。
编译前 (.vue):
vue
<script setup>
import { ref } from 'vue'
defineProps(['foo'])
const msg = ref('Hello')
function log() { console.log(msg.value) }
</script>
<template>
<div @click="log">{{ msg }} {{ foo }}</div>
</template>编译后 (.js) (概念):
javascript
import { defineComponent, ref, toDisplayString, ... } from 'vue'
export default /*@__PURE__*/defineComponent({
props: ['foo'], // <--- defineProps 的结果
setup(__props, { expose }) {
// 1. 你的所有代码都被“封装”进来了
const msg = ref('Hello')
function log() { console.log(msg.value) }
// 2. 自动 return,供模板使用
return (_ctx, _cache) => {
return (
_openBlock(),
_createElementBlock("div", { onClick: log },
_toDisplayString(_ctx.msg) + " " + _toDisplayString(_ctx.foo),
)
)
}
}
})这个“翻译”工作由 compileScript (@vue/compiler-sfc/src/compileScript.ts) 负责。
compileScript:封装 setup 函数
compileScript 的核心是创建了一个 ScriptCompileContext 上下文,然后将 <script setup> 块的所有内容(导入、变量、函数)解析为 AST(抽象语法树),并“提升”(Lift)到新生成的 setup 函数的内部。
typescript
// core/packages/compiler-sfc/src/compileScript.ts
export function compileScript(
sfc: SFCDescriptor,
options: SFCScriptCompileOptions,
): SFCScriptBlock {
// ...
const ctx = new ScriptCompileContext(sfc, options)
const scriptSetupAst = ctx.scriptSetupAst!
// 1. 遍历 <script setup> 的顶层
for (const node of scriptSetupAst.body) {
if (node.type === 'ImportDeclaration') {
// 收集导入
} else if (isMacroCall(node)) {
// 2. 【关键】处理“宏”
if (processDefineProps(ctx, node) ||
processDefineEmits(ctx, node) ||
processDefineExpose(ctx, node)) {
// 如果是宏,处理完就“抹掉”
ctx.s.remove(node.start!, node.end!)
}
} else {
// 3. 其他所有代码 (变量、函数) 都被视为
// setup() 函数体的一部分
}
}
// 4. 【封装】
// 最后,在 <script setup> 内容的“前后”
// 插入 "defineComponent({ setup() { ... } })" 包装器
ctx.s.prependLeft(startOffset, `\nexport default ${ctx.helper('defineComponent')}({
${/* ... 注入 props/emits ... */ }
${hasAwait ? 'async ' : ''}setup() {
`)
ctx.s.appendRight(endOffset, `
return { ${/* ... 自动 return ... */ } }
}
})`)
return { ... }
}“宏” (Macros):编译器的“秘密指令”
defineProps 和 defineEmits 不是真正的运行时函数,它们是只在编译时存在的“宏”。compileScript 会主动查找这些“秘密指令”。
typescript
// core/packages/compiler-sfc/src/script/macros.ts
// “宏”列表
const MACROS = [
'defineProps',
'defineEmits',
'defineExpose',
'defineOptions', // (3.3+)
'defineModel', // (3.4+)
'withDefaults'
]
// processDefineProps (简化)
export function processDefineProps(
ctx: ScriptCompileContext,
node: Node,
): boolean {
if (!isCallOf(node, DEFINE_PROPS)) { // 检查是否是 defineProps()
return false
}
// 1. 记录:我们找到了 defineProps()
ctx.hasDefinePropsCall = true
// 2. 提取“运行时”声明
// e.g., defineProps(['foo']) -> 提取 ['foo']
ctx.propsRuntimeDecl = node.arguments[0]
// 3. 提取“类型”声明
// e.g., defineProps<{ foo: string }>()
if (node.typeParameters) {
ctx.propsTypeDecl = node.typeParameters.params[0]
}
return true // 告诉 compileScript:“处理完毕,请抹掉这行代码”
}compileScript 找到 defineProps,“偷”走它的参数(['foo'])并将其存起来,用于生成 export default 对象的 props: ['foo'] 选项。
defineProps 之所以不需要 import,就是因为它根本不是一个 JS 函数,而是给编译器的“密令”。
CSS v-bind:响应式的 CSS 变量
<style> 中的 v-bind() 是 <script setup> 之后最“神奇”的编译特性。它打通了“JS 响应式状态”和“CSS 样式”之间的壁垒。
这个魔法分两步实现:
- 编译时(
compileStyle):将 CSSv-bind()重写为 CSS 变量(var(--...))。 - 代码生成(
compileScript):注入 JS 代码 (_useCssVars),在运行时更新这个 CSS 变量。
1. 编译时 (compileStyle):重写 CSS
当 @vue/compiler-sfc 处理 <style> 块时,它会使用 PostCSS 和一个自定义插件 cssVarsPlugin。
编译前 (.vue):
css
.text {
color: v-bind(myColor);
}cssVarsPlugin (packages/compiler-sfc/src/cssVars.ts) 的工作流:
typescript
// 1. 正则:找到 "v-bind("
const vBindRE = /v-bind\s*\(/g
export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
return {
postcssPlugin: 'vue-sfc-vars',
Declaration(decl) { // 遍历所有 CSS 声明 (e.g., color: ...)
const value = decl.value
if (vBindRE.test(value)) {
// 2. 找到了 "v-bind("
// 3. 词法分析:安全地提取括号内的表达式
// v-bind(myColor) -> "myColor"
// v-bind('myColor + "px"') -> "myColor + 'px'"
const variable = normalizeExpression(content.slice(start, end))
// 4. 生成一个“哈希”
const hash = genVarName(id, variable, isProd) // e.g., "7a7a37b1-myColor"
// 5. 【重写】
// 将 "color: v-bind(myColor)"
// 重写为 "color: var(--7a7a37b1-myColor)"
decl.value = value.replace(vBindRE, `var(--${hash})`)
// 6. 记录这笔“账”:
// (变量 "myColor" 对应 CSS 变量 "--7a7a37b1-myColor")
sfc.cssVars.push(variable)
}
},
}
}编译后 (.css):
css
.text[data-v-7a7a37b1] {
color: var(--7a7a37b1-myColor);
}2. 代码生成 (compileScript):注入 JS 运行时
compileStyle 已经完成了 CSS 的“重写”,并把 sfc.cssVars 列表(['myColor'])交还给了 compileScript。
compileScript 现在必须注入 JS 代码,以便在 myColor 变化时,去更新那个 CSS 变量。
typescript
// core/packages/compiler-sfc/src/compileScript.ts
// 在 compileScript 的末尾
if (sfc.cssVars.length) {
// 【注入】
// 1. 生成 CSS 变量映射对象:
// e.g., "{\n '--7a7a37b1-myColor': (myColor)\n}"
const varsExp = genCssVarsFromList(sfc.cssVars, id, isProd)
// 2. 生成对 _useCssVars 辅助函数的调用
// _useCssVars(_ctx => ({ '--7a7a37b1-myColor': (_ctx.myColor) }))
const code = `_${CSS_VARS_HELPER}(_ctx => (${varsExp}))`
// 3. 将这行代码插入到生成的 setup() 函数的末尾
ctx.s.append(code)
}3. 运行时(_useCssVars):effect 的力量
编译后的 setup (概念):
javascript
import { defineComponent, ref, useCssVars as _useCssVars } from 'vue'
export default /*@__PURE__*/defineComponent({
setup(__props, { expose }) {
const myColor = ref('red')
// 【注入的运行时代码】
_useCssVars(_ctx => ({
'--7a7a37b1-myColor': (_ctx.myColor)
}))
return ...
}
})_useCssVars (packages/runtime-dom/src/helpers/useCssVars.ts) 才是“魔法”的最后一环:
typescript
// core/packages/runtime-dom/src/helpers/useCssVars.ts
export function useCssVars(
getter: (ctx: any) => Record<string, string>,
// ...
) {
const instance = getCurrentInstance()
// 1. 获取当前组件的 DOM 根元素
const el = instance.vnode.el as HTMLElement
// 2. 【核心】创建一个 effect,
// 它会“订阅” getter 中用到的所有响应式数据
watchEffect(() => {
// 3. 执行 getter,获取“新”的 CSS 变量值
// e.g., { '--7a7a37b1-myColor': 'red' }
const vars = getter(instance.proxy)
// 4. 【更新 DOM】
// 遍历 vars,将它们设置为 CSS 自定义属性
for (const key in vars) {
el.style.setProperty(key, vars[key])
}
})
}总结:v-bind(myColor) 在编译时被换成 var(--hash);同时编译器注入 useCssVars,它会 watchEffect myColor 的变化,一旦变化,就通过 el.style.setProperty 更新 --hash 的值,从而实现了 JS 状态到 CSS 的响应式链接。
总结
Vue 3 的 SFC 编译系统(@vue/compiler-sfc)是 Vue “开发者体验”和“高性能”的基石:
<script setup>:是一个“代码封装”的魔法。compileScript负责解析宏(defineProps),并将“顶层”代码自动封装进setup()函数中,实现了零样板代码的组合式 API。- CSS
v-bind():是一个“编译 + 运行时”协同的魔法。compileStyle负责将v-bind(myColor)重写为var(--hash),同时compileScript负责注入useCssVars。useCssVars在运行时创建effect,订阅myColor的变化,并命令式地更新 CSS 变量,实现了 JS 到 CSS 的响应式。
