Skip to content

: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):编译器的“秘密指令”

definePropsdefineEmits 不是真正的运行时函数,它们是只在编译时存在的“”。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 样式”之间的壁垒。

这个魔法分两步实现:

  1. 编译时(compileStyle:将 CSS v-bind() 重写为 CSS 变量(var(--...))。
  2. 代码生成(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 “开发者体验”和“高性能”的基石:

  1. <script setup>:是一个“代码封装”的魔法。compileScript 负责解析宏(defineProps),并将“顶层”代码自动封装setup() 函数中,实现了零样板代码的组合式 API。
  2. CSS v-bind():是一个“编译 + 运行时”协同的魔法。compileStyle 负责将 v-bind(myColor) 重写var(--hash),同时 compileScript 负责注入 useCssVarsuseCssVars 在运行时创建 effect订阅 myColor 的变化,并命令式地更新 CSS 变量,实现了 JS 到 CSS 的响应式。

微信公众号二维码

Last updated: