一、概念层:什么是"宏函数"?
在 <script setup> 中,Vue 引入了一系列编译时宏(compiler macros) ,例如:
| 宏函数 | 功能 |
|---|---|
defineProps() |
声明组件接收的 props |
defineEmits() |
声明组件的事件 |
defineExpose() |
控制组件暴露给父组件的属性 |
defineSlots() |
类型声明插槽接口 |
defineOptions() |
编译期配置组件选项(如 name) |
defineModel() |
双向绑定语法糖(Vue 3.3+) |
withDefaults() |
给 defineProps 的结果设置默认值 |
这些宏不是普通函数------
它们不会在运行时执行 ,而是由 compileScript() 在编译阶段识别、消解并转译成等价的运行时代码。
二、原理层:宏的检测与解析流程
宏函数的识别核心逻辑在以下片段中:
scss
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)
} else if (processDefineExpose(ctx, expr)) {
const callee = (expr as CallExpression).callee
ctx.s.overwrite(
callee.start! + startOffset,
callee.end! + startOffset,
'__expose',
)
} else {
processDefineModel(ctx, expr)
}
}
}
我们来分解这段代码的逻辑流程。
(1)AST 遍历与宏检测
- 遍历
<script setup>的所有语句节点; - 对每个表达式节点调用
unwrapTSNode()去除类型包裹; - 然后依次调用
processDefineProps()等函数。
每个 processDefineXXX() 都会:
- 检查是否为目标宏调用;
- 提取参数与类型;
- 记录编译时信息到
ctx; - 视情况修改源码字符串(删除、替换、移动)。
例如:
c
defineProps<{ foo: string }>()
在编译时会被转化为:
css
__props: { foo: String }
并在 setup() 的参数中体现。
(2)宏函数识别的关键:isCallOf()
源码内部使用一个通用检测工具:
scss
isCallOf(node, calleeName)
它会检查一个 AST 节点是否是某个函数调用,如:
scss
isCallOf(expr, DEFINE_PROPS)
=> 判断当前表达式是否为 defineProps(...) 调用。
如果匹配成功,processDefineProps() 就会进一步解析调用参数与类型注解。
三、对比层:编译期宏 vs 运行时 API
| 特征 | 宏函数(compile-time) | 运行时 API(runtime) |
|---|---|---|
| 执行阶段 | 编译时 | 运行时 |
| 是否存在于最终代码 | 否,被移除或替换 | 是,保留在代码中 |
| 是否可动态调用 | ❌ 不行(必须静态) | ✅ 可以动态执行 |
| 目的 | 简化语法 / 优化类型推断 | 执行逻辑 |
| 代表示例 | defineProps / defineEmits |
ref() / reactive() |
换句话说,
defineProps只是一个编译提示,它的结果不会真正存在于运行时。
四、实践层:defineProps 处理流程举例
以最常见的宏 defineProps() 为例:
示例输入
ini
<script setup lang="ts">
const props = defineProps<{ title: string; count?: number }>()
</script>
编译后结果(简化)
javascript
export default defineComponent({
props: {
title: String,
count: Number
},
setup(__props) {
const props = __props
return { props }
}
})
对应编译逻辑(processDefineProps() 概览)
ini
if (isCallOf(node, DEFINE_PROPS)) {
ctx.propsCall = node
ctx.propsDecl = variableDeclarationNode
ctx.propsTypeDecl = extractTypeParameter(node)
ctx.bindingMetadata[propName] = BindingTypes.SETUP_REACTIVE_CONST
return true
}
逐步说明:
-
检测宏调用:
- 判断
node是否为defineProps();
- 判断
-
记录声明节点:
- 将当前节点绑定到
ctx.propsDecl;
- 将当前节点绑定到
-
类型提取:
- 通过 TypeScript AST 分析
<T>泛型;
- 通过 TypeScript AST 分析
-
生成运行时代码:
- 在后续阶段调用
genRuntimeProps(ctx)将类型转为运行时对象;
- 在后续阶段调用
-
作用域注册:
- 在
ctx.bindingMetadata中登记绑定类型。
- 在
五、拓展层:宏函数的设计哲学
Vue 的宏系统并非简单语法糖,而是一种 "声明式编译接口" 设计思想。
它让编译器能在源代码层面直接读取开发者的意图,而不依赖复杂的运行时分析。
这带来了几个优点:
| 优点 | 说明 |
|---|---|
| 类型系统完美融合 | TypeScript 的类型能直接与宏配合使用。 |
| 零运行时开销 | 编译期完成替换,不产生额外函数调用。 |
| 语义清晰 | 一眼能看出代码意图:props、emits、expose。 |
| 更强的静态分析能力 | IDE 与模板编译器能直接读取这些信息。 |
六、潜在问题与限制
| 问题 | 描述 |
|---|---|
| 不能动态调用 | 宏函数必须静态存在,不能用变量包装或条件语句调用。 |
| 作用域限制 | 宏不能引用局部变量(会被提升到 setup() 外层)。 |
| 类型复杂度 | 当泛型类型太复杂时,类型提取可能失败。 |
| 编译器兼容性 | 不同版本的 Vue / vite 对宏语法的支持略有差异。 |
七、小结
这一篇我们理解了:
- 宏函数的作用与编译原理;
- 编译时如何识别、提取与替换;
defineProps等宏的典型转译过程;- 宏系统如何连接 TypeScript 类型与 Vue 的运行时代码。
在下一篇,我们将进入更底层的部分------
🔍 第 3 篇:《绑定分析与作用域推断》 ,
讲解 BindingTypes 与 analyzeScriptBindings() 如何推断每个变量的响应式类别(ref、const、reactive、prop 等)。
本文部分内容借助 AI 辅助生成,并由作者整理审核。