Vue script setup 中组件静默消失的原因:一次命名冲突排查(2):顶层绑定怎样参与模板名字解析
Vue script setup 中组件静默消失的原因:一次命名冲突排查 · 1 · 第 2 篇(当前) · 3
模板中的 popup-status 本来应渲染 PopupStatus 组件,同一个 script setup 里却还有 popupStatus 状态对象。Vue 3.5.34 的编译器先按绑定类型分组,再依次尝试原名、camelCase 和 PascalCase;较早命中的状态对象因此成为组件引用。
下面复用同一组名字,追踪 compiler-sfc 提供绑定元数据、compiler-core 生成 render 引用的过程。分析固定在文中源码版本;Vue 3.0、Vue 2.7 与早期第三方方言不能套用同一套结论,版本对照见故障定位篇。
为什么会优先命中普通变量
compileTemplate 根据顶层绑定的分类和候选名顺序,决定 <popup-status> 引用哪个变量。script 和 template 的编译通过下面这条调用链衔接:
@vue/compiler-sfc 负责 SFC 这层事情:把 .vue 拆成 template、script、style,处理 <script setup>,并整理出 bindingMetadata。它在这一步知道「有一个导入的 PopupStatus,还有一个本地的 popupStatus」,但它还没有决定模板里的 <popup-status> 最终应该用哪一个。
compileTemplate() 是 @vue/compiler-sfc 暴露出来的模板编译入口。它会带着 bindingMetadata 调用 @vue/compiler-core 的模板编译能力。@vue/compiler-core 负责把模板 AST 转成 render 函数;在这个问题里,<popup-status> 到底变成 $setup["popupStatus"] 还是 $setup["PopupStatus"],关键就在它的元素解析逻辑。
这个问题可以拆成两步:先看 bindingMetadata 怎么把 popupStatus 和 PopupStatus 分到不同类型,再看 compiler-core 怎么按这些类型和候选名做查找。
变量先被归类
PopupStatus 来自顶层 import,popupStatus 来自 <script setup> 里的本地变量。编译器分别为它们分类,再把分类结果汇总到同一份 bindingMetadata。
compileScript() 会先收集 <script> 和 <script setup> 里的 import,处理普通 <script>、<script setup> 和编译宏,完成 props 解构转换、宏参数作用域检查,并移除非 script 内容。随后进入绑定分析阶段,把前面收集到的 userImports、scriptBindings、setupBindings 合并成 bindingMetadata。
相关源码都在 Vue 3.5.34 的
packages/compiler-sfc/src/compileScript.ts里:scriptBindings/setupBindings初始化、walkDeclaration()写入普通<script>绑定、walkDeclaration()写入<script setup>绑定、bindingMetadata合并。下面只用整行注释省略不相关分支,保留源码原本的结构。
绑定元数据合并部分如下;顶层声明遍历与其他宏处理保留在完整节选中。
for (const [key, { isType, imported, source }] of Object.entries(ctx.userImports)) {
if (isType) continue
ctx.bindingMetadata[key] =
imported === '*' ||
(imported === 'default' && source.endsWith('.vue')) ||
source === 'vue'
? BindingTypes.SETUP_CONST
: BindingTypes.SETUP_MAYBE_REF
}
for (const key in scriptBindings) {
ctx.bindingMetadata[key] = scriptBindings[key]
}
for (const key in setupBindings) {
ctx.bindingMetadata[key] = setupBindings[key]
}
完整上下文见绑定分析源码节选弹窗。
这段里最重要的是两条分类入口:
- import 会在
ctx.userImports这一段直接分类,并写进ctx.bindingMetadata。 - 除 import 之外的顶层定义会先交给
walkDeclaration()分类,结果写进scriptBindings或setupBindings,最后再合并进ctx.bindingMetadata。
PopupStatus 来自 ctx.userImports,所以看这条 import 分支:只有命名空间 import、从 .vue 文件 default import、或者从 vue 包本身 import,才会被标成 SETUP_CONST。其他 import 都会落到 SETUP_MAYBE_REF。
popupStatus 来自 <script setup> 里的本地变量。它会先进入 setupBindings,而 setupBindings 的分类来自 walkDeclaration()。
walkDeclaration()的精确源码在packages/compiler-sfc/src/compileScript.ts。下面同样是完整判断结构的阅读版,省掉了对象 / 数组解构辅助函数的展开,但保留了变量、枚举、函数、类这些和本文表格相关的分类分支。
walkDeclaration() 的第三个参数 bindings 是分类结果的写入目标:普通 <script> 会传入 scriptBindings,<script setup> 会传入 setupBindings。后面的分支负责判断当前声明属于哪种 BindingTypes,并把结果写进这个对象。
这里只截取字面量、reactive 与可确定不是 Ref 的 const 分支;后续 Ref、解构和 enum 分支见完整节选。
const userReactiveBinding = userImportAliases['reactive']
if (
(hoistStatic || from === 'script') &&
(isAllLiteral || (isConst && isStaticNode(init!)))
) {
// const foo = 'x'
bindingType = BindingTypes.LITERAL_CONST
} else if (isCallOf(init, userReactiveBinding)) {
// const foo = reactive({})
bindingType = isConst
? BindingTypes.SETUP_REACTIVE_CONST
: BindingTypes.SETUP_LET
} else if (
isConstMacroCall ||
(isConst && canNeverBeRef(init!, userReactiveBinding))
) {
// const foo = {}
// const props = defineProps()
bindingType = isCallOf(init, DEFINE_PROPS)
? BindingTypes.SETUP_REACTIVE_CONST
: BindingTypes.SETUP_CONST
}
其他分支见完整绑定分类弹窗。
BindingTypes 是编译器记录在 bindingMetadata 里的绑定分类,描述它能否确定这个值是 ref、绑定是否可能被重新赋值,以及值能否静态处理。它不是 JavaScript 变量声明关键字的直接映射:const count = ref(0) 会归为 SETUP_REF,普通调用 const value = useFoo() 则是 SETUP_MAYBE_REF。
图中对应 Vue 3.5.34 的常见分类,定义见 BindingTypes 枚举,判定条件见 walkDeclaration()。字面量示例以只有 <script setup> 且开启 hoistStatic 为前提;关闭静态提升后,图中的 label 和 n 会归为 SETUP_CONST。
落到常见写法上,可以粗略这样看:
| 写法 | 分类 | 原因 |
|---|---|---|
import { ref } from 'vue' |
SETUP_CONST |
source === 'vue',编译器知道这是 Vue API |
import * as Vue from 'vue' |
SETUP_CONST |
imported === '*',命名空间 import 是稳定对象 |
import Foo from './Foo.vue' |
SETUP_CONST |
default import 且来源以 .vue 结尾,编译器能确定是组件模块 |
import { Foo } from './components' |
SETUP_MAYBE_REF |
barrel 具名导入,编译器不知道它最终是不是 ref |
const foo = {} |
SETUP_CONST |
普通对象字面量不可能是 ref |
const foo = reactive({}) |
SETUP_REACTIVE_CONST |
reactive() 结果是稳定响应式对象 |
const foo = ref(0) / computed(...) |
SETUP_REF |
明确由 ref 类 API 创建 |
const foo = useFoo() |
SETUP_MAYBE_REF |
普通函数调用结果可能是 ref,也可能不是 |
let foo = 1 |
SETUP_LET |
let 绑定运行时可能变化 |
const foo = 'x' |
LITERAL_CONST |
可静态提升的字面量常量 |
function foo() {} / class Foo {} |
SETUP_CONST |
函数 / 类声明是稳定绑定 |
enum Foo { A = 1 } |
LITERAL_CONST / SETUP_CONST |
成员都静态时可提升,否则按稳定绑定处理 |
回到这个例子:
import { PopupStatus } from './components';
这里的 imported 是 PopupStatus,source 是 ./components。它不是命名空间 import,不是从 .vue 文件 default import,也不是从 vue 包导入,所以会进入最后的 SETUP_MAYBE_REF。
而本地变量这边:
const popupStatus = { message: 'hello', type: 'success' };
它是普通对象字面量,不可能是 ref,所以会被 walkDeclaration() 归到 SETUP_CONST。
于是当前例子的 bindingMetadata 会变成这样:
{
"PopupStatus": "setup-maybe-ref",
"popupStatus": "setup-const"
}
到这里还没有发生错误。它只是把两个顶层绑定分到了不同类型里。
分类再参与解析
把 <popup-status> 解析成 $setup["popupStatus"] 的,是 @vue/compiler-core 里的 resolveSetupReference(),源码位于 Vue 3.5.34 的 packages/compiler-core/src/transforms/transformElement.ts。
同一绑定类型下先检查原名、camelCase,再检查 PascalCase。下面保留名字检查与 const 返回的节选,Ref 和 props 的回退分支放在材料中。
const camelName = camelize(name)
const PascalName = capitalize(camelName)
const checkType = (type: BindingTypes) => {
if (bindings[name] === type) {
return name
}
if (bindings[camelName] === type) {
return camelName
}
if (bindings[PascalName] === type) {
return PascalName
}
}
const fromConst =
checkType(BindingTypes.SETUP_CONST) ||
checkType(BindingTypes.SETUP_REACTIVE_CONST) ||
checkType(BindingTypes.LITERAL_CONST)
if (fromConst) {
return context.inline
? fromConst
: `$setup[${JSON.stringify(fromConst)}]`
}
完整函数见组件候选解析源码弹窗。
这里有两层顺序。
第一层是绑定类型顺序。SETUP_CONST、SETUP_REACTIVE_CONST、LITERAL_CONST 这一组在 fromConst 里先检查;SETUP_LET、SETUP_REF、SETUP_MAYBE_REF 这一组在 fromMaybeRef 里后检查;PROPS 最后在 fromProps 里检查。
第二层是同一个类型内部的候选名顺序。<popup-status> 对应的候选名依次是 popup-status、popupStatus、PopupStatus。
检查单个类型时,Vue 会按「原名 -> camelCase -> PascalCase」的顺序查。对 <popup-status> 来说,原名 popup-status 通常不会命中,第二个候选 popupStatus 会先于第三个候选 PopupStatus 被检查。
按绑定类型的检查顺序,逐个代入原名、camelCase 和 PascalCase 候选,本例的查找过程如下:
所以 <popup-status> 会先命中 popupStatus,而不是 PopupStatus。
图中灰色项表示本例在前面命中后没有继续检查;const-like 内部的各个 BindingTypes 也有先后顺序。
fromConst、fromMaybeRef 和 fromProps 的分支既决定排序,也对应不同的代码生成方式。当前实验产物是非 inline template 模式,所以 fromConst 和 fromMaybeRef 最后都会生成 $setup[...];源码仍然保留两个分支,是因为 inline template 模式下,可能是 ref 的绑定需要显式生成 unref(xxx)。
严格说,这里有两个相邻名字:对外的 SFC 编译选项叫
inlineTemplate,compiler-core 里真正影响resolveSetupReference()的选项叫inline。compileScript()打开inlineTemplate后,会在内部调用compileTemplate(),并把compilerOptions.inline设成true。inline template 的作用,是把 template 编译出来的 render 函数直接放进
setup()里。这样 render 可以直接访问setup()的局部变量,不需要先把所有绑定整理成一个返回对象再交给外部 render 访问。它更偏生产构建优化,不是业务代码里的功能开关。代价是 template 不能再和组件状态分开热更新。Vue 官方源码也把这一点写在
inlineTemplate注释里:它只影响<script setup>,并且应该只在生产使用,因为会阻止 template 和组件状态分开 HMR。如果直接使用
@vue/compiler-sfc,开关就是compileScript()的inlineTemplate:compileScript(descriptor, { inlineTemplate: true }) // 开启 compileScript(descriptor, { inlineTemplate: false }) // 关闭普通 Vite 项目里通常不在业务代码里手动开关,而是由
@vitejs/plugin-vue决定:开发服务下关闭,方便 template 单独热更新;生产构建里,如果是<script setup>且 template 不是外部src,插件会打开 inline template。最新插件源码里还会避开 Vue DevTools 开启的场景。这就是它需要单独处理 maybe-ref 的原因:render 如果直接访问
setup()里的局部变量,拿到的就是原始局部变量本身。对于SETUP_MAYBE_REF这类绑定,编译器只能知道它「可能是 ref」,不能在编译时确定运行时值是不是 ref。为了保持模板里 ref 自动解包的语义,inline 分支会生成unref(xxx)。非 inline template 不直接访问
setup()局部变量,而是通过$setup[...]访问 setup 暴露给 render 的对象,所以resolveSetupReference()在这一支里不生成unref()。启用 SFC 的
inlineTemplate时,compileScript()会将模板编译器的compilerOptions.inline设为true;普通 Vite 项目默认由@vitejs/plugin-vue的自动判断 决定是否开启这个选项。UNREF本身表示 compiler-core 里的 runtime helper symbol,并在helperNameMap里映射到字符串unref。context.helperString(UNREF)会先登记这个 helper,再返回生成代码里使用的名字_unref,最终对应import { unref as _unref } from 'vue'这类产物。
回到当前问题,inline 差异不是根因,只是解释了为什么源码需要分成不同返回分支。影响结果的仍然是分组顺序:setup-const 组先查到 popupStatus 以后,后面的 setup-maybe-ref 组和 props 组就不会再继续检查。
再往外看一层,resolveSetupReference() 也不是组件 tag 的全部解析逻辑。resolveComponentType() 会先处理动态组件和内置组件,再尝试从 setup 绑定里解析;这里的 setup 绑定包括 SETUP_CONST、SETUP_LET、SETUP_REF、SETUP_MAYBE_REF 这几类,也包括 PROPS。如果这些都没命中,才会继续走文件名推断出来的自引用组件,最后生成 resolveComponent(tag),交给运行时去查局部注册和全局注册的组件。
这条外层顺序在
resolveComponentType()里。运行时局部 / 全局组件查询在resolveAsset()里。
全局组件不靠 bindingMetadata 判断。它们通常会走最后的运行时解析分支:编译产物里出现 resolveComponent("xxx"),运行时再从当前组件的局部 components 和 appContext.components 里按原名、camelCase、PascalCase 查找。
全局函数又是另一条链路。模板表达式里的函数调用由表达式转换处理;如果名字能在 setup 绑定或 props 里找到,就按对应绑定类型生成访问代码。如果找不到,它会落到 _ctx.xxx 这类实例上下文访问上,再由运行时组件代理去解析。它不属于组件 tag 的 resolveComponentType() 这条链路。
表达式里的变量访问逻辑在
transformExpression.ts里;它和组件 tag 的resolveComponentType()是两条不同路径。
组件 tag 先按外层解析顺序查找;进入 setup 绑定分支后,再按内部的绑定类型分组查找:
第一,如果是 prop 和导入组件同名,通常不会抢过导入组件。例如同时存在 import { PopupStatus } ... 和 defineProps<{ popupStatus: unknown }>(),<popup-status> 会先在 fromMaybeRef 里命中 PopupStatus,不会走到最后的 fromProps。
第二,如果没有导入组件,只有一个同名 prop,情况就不同。popupStatus 作为 PROPS 会参与组件 tag 解析,而且它会早于运行时注册组件解析命中。此时 <popup-status> 会被编译成类似:
_createBlock(_unref($props["popupStatus"]))
因此,prop 也可能挡住运行时注册组件,包括全局组件。运行时注册组件只有在 setup 绑定、props、自引用组件都没命中时,才会进入 resolveComponent("popup-status") 这条解析链路。
第三,如果说的是 app.config.globalProperties 上挂的全局函数或全局变量,它不走组件 tag 的解析路径,而是走模板表达式解析。表达式解析里的大方向也类似:本地 setup 绑定和 props 先参与匹配,全局属性最后才通过组件实例上下文兜底。它出现在模板表达式里时,例如 {{ popupStatus() }},找不到本地 setup 绑定或 prop 时才会落到:
_ctx.popupStatus()
所以全局函数 / 全局变量的冲突重点不在 <popup-status> 这种组件 tag,而在模板表达式名。只要本地 setup 绑定或 prop 有同名标识符,就会优先按本地绑定处理,不会优先访问 _ctx 上的全局属性。
即使改成直接从 .vue 文件 default import,结果也还是不对。这样只会让 PopupStatus 也进入 setup-const 这一组,但同一组类型内部仍然会先检查 camelCase 的 popupStatus,再检查 PascalCase 的 PopupStatus。只要本地还存在 const popupStatus = ...,<popup-status> 仍然会先命中普通对象。
直接 .vue default import 只解决了「类型分组」差异,没有解决「候选名顺序」冲突。要修的是命名冲突本身。
Vue 编译阶段允许 popupStatus 和 PopupStatus 这两个合法的顶层绑定共存,并按既定顺序为 <popup-status> 选中一个。compiler-core 没有将多候选命中识别成冲突,也不知道开发者原本想渲染的是组件 PopupStatus。
等到运行时,选中的状态对象缺少模板和 render 函数,Vue 才给出对应 warning。这条提示说明的是对象无法渲染,却没有指出名字解析已经命中了另一个绑定,排查时还需要回到编译产物。
组件之外还有没有同类问题
判断这类问题时,先看同一个模板名字是否会进入 resolveSetupReference(),并按 raw / camel / Pascal 去 <script setup> 顶层绑定里找候选。按这个标准看,真正同源的是自定义指令。
<template>
<input v-popup-status />
</template>
<script setup lang="ts">
import { VPopupStatus } from './directives'
const vPopupStatus = {
mounted() {},
}
</script>
Vue compiler-core 的 buildDirectiveArgs() 会把自定义指令名拼成 v- + dir.name,再交给 resolveSetupReference()。对 v-popup-status 来说,候选名就是 v-popup-status、vPopupStatus、VPopupStatus。如果同一个 SFC 里同时有 vPopupStatus 和 VPopupStatus,当前解析会先命中 vPopupStatus。
这个场景比组件 tag 少见,因为局部指令本来就推荐写成 vFocus 这种 lower camel 形态;但它和组件 tag 共享同一段编译器机制,Lint 规则把它纳入检查是合理的。
静态事件名不属于这一类:
<Child @save-user="saveUser" />
save-user 会被转成 prop key onSaveUser;真正进入表达式作用域解析的是右侧 handler 表达式 saveUser。即使同一个 SFC 里存在 saveUser 和 SaveUser,事件名本身也不会去 setup 里找这两个候选。@[eventName] 这种动态事件名本来就是表达式,风险属于普通变量命名和类型约束,不属于组件 tag 这类归一化候选冲突。
props / attrs / v-model 参数、slot 名、template ref 也要分开看。它们可能有大小写规范或运行时归一化,例如 prop key 匹配和 v-model:foo-bar 生成 onUpdate:fooBar,但它们不是「同一个模板名字在 setup 顶层绑定里查多个候选」。因此这条规则适合覆盖组件 tag 和自定义指令,不适合把事件名、props、slot、ref 都塞进去。
总结
compileScript 收集 import 与顶层声明,形成 bindingMetadata;compileTemplate 再把这份分类交给 compiler-core。组件标签进入 setup 绑定解析后,先按绑定组查找,每组内部按原名、camelCase、PascalCase 尝试,命中后停止。
把组件改为直接 .vue 默认导入,只会改变绑定分组,不能消除组内的候选名冲突。自定义指令也进入同一条解析链,事件名、props 参数、slot 和 ref 则要按各自机制判断;检测规则应与这些实际分支保持一致。
上一篇:从静默消失定位到命名冲突。下一篇:检测组件与指令的绑定冲突。
绑定登记与分类的源码节选
// ...省略 compileScript 进入绑定分析前的准备步骤:
// 收集 import、处理普通 <script>、处理 <script setup> 和编译宏、
// props 解构转换、宏参数作用域检查、移除非 script 内容。
const scriptBindings: Record<string, BindingTypes> = Object.create(null)
const setupBindings: Record<string, BindingTypes> = Object.create(null)
if (scriptAst) {
for (const node of scriptAst.body) {
// ...省略普通 <script> 的 default export、具名导出和位置移动处理。
if (node.declaration) {
walkDeclaration(
'script',
node.declaration,
scriptBindings,
vueImportAliases,
hoistStatic,
)
} else if (
// 普通 <script> 顶层变量、函数、类、TypeScript enum。
(node.type === 'VariableDeclaration' ||
node.type === 'FunctionDeclaration' ||
node.type === 'ClassDeclaration' ||
node.type === 'TSEnumDeclaration') &&
!node.declare
) {
walkDeclaration('script', node, scriptBindings, vueImportAliases, hoistStatic)
}
}
}
for (const node of scriptSetupAst.body) {
// ...省略 <script setup> 的 defineProps / defineEmits / defineModel 等宏处理。
if (
// <script setup> 顶层变量、函数、类、TypeScript enum。
(node.type === 'VariableDeclaration' ||
node.type === 'FunctionDeclaration' ||
node.type === 'ClassDeclaration' ||
node.type === 'TSEnumDeclaration') &&
!node.declare
) {
walkDeclaration(
'scriptSetup',
node,
setupBindings,
vueImportAliases,
hoistStatic,
!!ctx.propsDestructureDecl,
)
}
}
// 6. analyze binding metadata
// `defineProps` & `defineModel` also register props bindings
if (scriptAst) {
Object.assign(ctx.bindingMetadata, analyzeScriptBindings(scriptAst.body))
}
for (const [key, { isType, imported, source }] of Object.entries(ctx.userImports)) {
if (isType) continue
ctx.bindingMetadata[key] =
imported === '*' ||
(imported === 'default' && source.endsWith('.vue')) ||
source === 'vue'
? BindingTypes.SETUP_CONST
: BindingTypes.SETUP_MAYBE_REF
}
for (const key in scriptBindings) {
ctx.bindingMetadata[key] = scriptBindings[key]
}
for (const key in setupBindings) {
ctx.bindingMetadata[key] = setupBindings[key]
}
// ...省略 v-model 绑定降级、useCssVars、setup 参数和 default export 生成。
walkDeclaration 的完整节选
// ...省略函数参数类型、AST 类型守卫和解构辅助函数。
function walkDeclaration(
from: 'script' | 'scriptSetup',
node: Declaration,
bindings: Record<string, BindingTypes>,
userImportAliases: Record<string, string>,
hoistStatic: boolean,
isPropsDestructureEnabled = false,
): boolean {
let isAllLiteral = false
if (node.type === 'VariableDeclaration') {
const isConst = node.kind === 'const'
isAllLiteral =
isConst &&
node.declarations.every(
decl => decl.id.type === 'Identifier' && isStaticNode(decl.init!),
)
for (const { id, init: _init } of node.declarations) {
const init = _init && unwrapTSNode(_init)
const isConstMacroCall =
isConst &&
isCallOf(
init,
c =>
c === DEFINE_PROPS ||
c === DEFINE_EMITS ||
c === WITH_DEFAULTS ||
c === DEFINE_SLOTS,
)
if (id.type === 'Identifier') {
let bindingType: BindingTypes
const userReactiveBinding = userImportAliases['reactive']
if (
(hoistStatic || from === 'script') &&
(isAllLiteral || (isConst && isStaticNode(init!)))
) {
// const foo = 'x'
bindingType = BindingTypes.LITERAL_CONST
} else if (isCallOf(init, userReactiveBinding)) {
// const foo = reactive({})
bindingType = isConst
? BindingTypes.SETUP_REACTIVE_CONST
: BindingTypes.SETUP_LET
} else if (
isConstMacroCall ||
(isConst && canNeverBeRef(init!, userReactiveBinding))
) {
// const foo = {}
// const props = defineProps()
bindingType = isCallOf(init, DEFINE_PROPS)
? BindingTypes.SETUP_REACTIVE_CONST
: BindingTypes.SETUP_CONST
} else if (isConst) {
if (
isCallOf(
init,
m =>
m === userImportAliases['ref'] ||
m === userImportAliases['computed'] ||
m === userImportAliases['shallowRef'] ||
m === userImportAliases['customRef'] ||
m === userImportAliases['toRef'] ||
m === userImportAliases['useTemplateRef'] ||
m === DEFINE_MODEL,
)
) {
// const foo = ref(0)
// const foo = computed(...)
bindingType = BindingTypes.SETUP_REF
} else {
// const foo = useFoo()
bindingType = BindingTypes.SETUP_MAYBE_REF
}
} else {
// let foo = 1
bindingType = BindingTypes.SETUP_LET
}
registerBinding(bindings, id, bindingType)
} else {
if (isCallOf(init, DEFINE_PROPS) && isPropsDestructureEnabled) {
continue
}
// const { foo } = xxx / const [foo] = xxx
// 解构变量会继续按 const / let / defineProps 等信息登记。
if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings, isConst, isConstMacroCall)
} else if (id.type === 'ArrayPattern') {
walkArrayPattern(id, bindings, isConst, isConstMacroCall)
}
// ...省略 walkObjectPattern / walkArrayPattern 的完整展开。
}
}
} else if (node.type === 'TSEnumDeclaration') {
isAllLiteral = node.members.every(
member => !member.initializer || isStaticNode(member.initializer),
)
bindings[node.id!.name] = isAllLiteral
? BindingTypes.LITERAL_CONST
: BindingTypes.SETUP_CONST
} else if (
node.type === 'FunctionDeclaration' ||
node.type === 'ClassDeclaration'
) {
// export function foo() {} / export class Foo {}
bindings[node.id!.name] = BindingTypes.SETUP_CONST
}
return isAllLiteral
}组件候选解析的源码节选
function resolveSetupReference(name: string, context: TransformContext) {
const bindings = context.bindingMetadata
if (!bindings || bindings.__isScriptSetup === false) {
return
}
const camelName = camelize(name)
const PascalName = capitalize(camelName)
const checkType = (type: BindingTypes) => {
if (bindings[name] === type) {
return name
}
if (bindings[camelName] === type) {
return camelName
}
if (bindings[PascalName] === type) {
return PascalName
}
}
const fromConst =
checkType(BindingTypes.SETUP_CONST) ||
checkType(BindingTypes.SETUP_REACTIVE_CONST) ||
checkType(BindingTypes.LITERAL_CONST)
if (fromConst) {
return context.inline
? fromConst
: `$setup[${JSON.stringify(fromConst)}]`
}
const fromMaybeRef =
checkType(BindingTypes.SETUP_LET) ||
checkType(BindingTypes.SETUP_REF) ||
checkType(BindingTypes.SETUP_MAYBE_REF)
if (fromMaybeRef) {
return context.inline
? `${context.helperString(UNREF)}(${fromMaybeRef})`
: `$setup[${JSON.stringify(fromMaybeRef)}]`
}
const fromProps = checkType(BindingTypes.PROPS)
if (fromProps) {
return `${context.helperString(UNREF)}(${
context.inline ? '__props' : '$props'
}[${JSON.stringify(fromProps)}])`
}
}