前言#
上篇文章中我们分析了是如何处理setup block的ast的,这篇文章我们接着往下分析,如果没看过可以去看下~。
坏蛋Dan:vue/compiler-sfc源码分析学习--part2:如何处理script--day3
正文#
老规矩我们说的non setup block或者non setup script block指的是<script>...</script>这样,反之则是<script setup>...</script>
测试单元#
老样子
收集ref语法糖变量#
// 3. Apply reactivity transform
if (
(enableReactivityTransform &&
// normal <script> had ref bindings that maybe used in <script setup>
(refBindings || shouldTransform(scriptSetup.content))) ||
propsDestructureDecl
) {
const { rootRefs, importedHelpers } = transformAST(
scriptSetupAst,
s,
startOffset,
refBindings,
propsDestructuredBindings
)
refBindings = refBindings ? [...refBindings, ...rootRefs] : rootRefs
for (const h of importedHelpers) {
helperImports.add(h)
}
}这个就不多说了,前面的文章也说过了。就是收集使用了$ref这种语法糖的变量,不过这里还有一个点需要看下,propsDestructuredBindings把这东西也传进去了。propsDestructuredBindings: 是之前我们收集到的props解构声明变量集合。把这个东西传进去可能是有什么操作需要,所以我们需要分析下里面这里做了什么才行。分析放其它中,因为不是这个包中的代码。这里简单的说下即可,简单的说就是和处理$ref一样,需要将解构的prop处理成__prop.xxx,因为这样才不会失去响应式。
处理并收集runtime props#
if (propsTypeDecl) {
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes, isProd)
}
function extractRuntimeProps(
node: TSTypeLiteral | TSInterfaceBody,
props: Record<string, PropTypeData>,
declaredTypes: Record<string, string[]>,
isProd: boolean
) {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
for (const m of members) {
if (
(m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature') &&
m.key.type === 'Identifier'
) {
let type
if (m.type === 'TSMethodSignature') {
type = ['Function']
} else if (m.typeAnnotation) {
type = inferRuntimeType(m.typeAnnotation.typeAnnotation, declaredTypes)
}
props[m.key.name] = {
key: m.key.name,
required: !m.optional,
type: type || [`null`]
}
}
}
}propsTypeDecl: 上篇文章中我们在分析processDefineProps时有提到过,这个变量是针对typescript的only-type[1]场景的,存放的是props的ts类型声明的节点,只要是ts的东西,除了enum,几乎都得在编译的时候就处理掉。typeDeclaredProps:props的类型声明的集合,此时这里还是空的。declaredProps: 上篇末尾有提到的,如果是typescript下的会进行收集类型定义并存放到这个变量中,然后移除源码中相关的代码,毕竟浏览器不认识ts代码。isProd: 是否是生产环境。interRuntimeType: 看下代码
function inferRuntimeType(
node: TSType,
declaredTypes: Record<string, string[]>
): string[] {
switch (node.type) {
case 'TSStringKeyword':
return ['String']
case 'TSNumberKeyword':
return ['Number']
case 'TSBooleanKeyword':
return ['Boolean']
case 'TSObjectKeyword':
return ['Object']
case 'TSTypeLiteral':
// TODO (nice to have) generate runtime property validation
return ['Object']
case 'TSFunctionType':
return ['Function']
case 'TSArrayType':
case 'TSTupleType':
// TODO (nice to have) generate runtime element type/length checks
return ['Array']
case 'TSLiteralType':
switch (node.literal.type) {
case 'StringLiteral':
return ['String']
case 'BooleanLiteral':
return ['Boolean']
case 'NumericLiteral':
case 'BigIntLiteral':
return ['Number']
default:
return [`null`]
}
case 'TSTypeReference':
if (node.typeName.type === 'Identifier') {
if (declaredTypes[node.typeName.name]) {
return declaredTypes[node.typeName.name]
}
switch (node.typeName.name) {
case 'Array':
case 'Function':
case 'Object':
case 'Set':
case 'Map':
case 'WeakSet':
case 'WeakMap':
case 'Date':
case 'Promise':
return [node.typeName.name]
case 'Record':
case 'Partial':
case 'Readonly':
case 'Pick':
case 'Omit':
case 'Exclude':
case 'Extract':
case 'Required':
case 'InstanceType':
return ['Object']
}
}
return [`null`]
case 'TSParenthesizedType':
return inferRuntimeType(node.typeAnnotation, declaredTypes)
case 'TSUnionType':
return [
...new Set(
[].concat(
...(node.types.map(t => inferRuntimeType(t, declaredTypes)) as any)
)
)
]
case 'TSIntersectionType':
return ['Object']
case 'TSSymbolKeyword':
return ['Symbol']
default:
return [`null`] // no runtime check
}
} 看名字就知道,这是用来定义runtime时的类型,typescript的类型都会被转换为浏览器知道的类型。
这一个小标题的代码很好理解,就是收集props类型声明,也就是上面说的typeDeclaredProps并统一格式为{ key, required, type }。
处理并收集runtime emits#
if (emitsTypeDecl) {
extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
}
function extractRuntimeEmits(
node: TSFunctionType | TSTypeLiteral | TSInterfaceBody,
emits: Set<string>
) {
if (node.type === 'TSTypeLiteral' || node.type === 'TSInterfaceBody') {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
for (let t of members) {
if (t.type === 'TSCallSignatureDeclaration') {
extractEventNames(t.parameters[0], emits)
}
}
return
} else {
extractEventNames(node.parameters[0], emits)
}
}
function extractEventNames(
eventName: Identifier | RestElement,
emits: Set<string>
) {
if (
eventName.type === 'Identifier' &&
eventName.typeAnnotation &&
eventName.typeAnnotation.type === 'TSTypeAnnotation'
) {
const typeNode = eventName.typeAnnotation.typeAnnotation
if (typeNode.type === 'TSLiteralType') {
if (typeNode.literal.type !== 'UnaryExpression') {
emits.add(String(typeNode.literal.value))
}
} else if (typeNode.type === 'TSUnionType') {
for (const t of typeNode.types) {
if (
t.type === 'TSLiteralType' &&
t.literal.type !== 'UnaryExpression'
) {
emits.add(String(t.literal.value))
}
}
}
}
}我把调用的方法都放进来了,代码有些长,但是挺好理解的。
emitsTypeDecl: 这里自然指的是被收集的emits的only-type[2]类型声明的节点TSInterfaceBody[3]: 看名字就知道是typescript的interface类型声明的主体。TSCallSignatureDeclaration: 调用函数声明。。这是我个人的翻译。。比如这样的
interface Test {
(a: number, b: string): void;
}TSTypeAnnotation[4]: 函数参数类型声明,就是上边这个number/string。TSLiteralType[5]: 比如这种type A = "test",literal指的是字面的,也就是说写死的数字\字符串等都符合这个类型。TSUnionType[6]: 比如这种a: number | string。
其他类型之前都讲过了,这里就不多赘述。
这个小标题也挺简单的, 和上面一样,也是在处理并收集emit的类型定义,不过这里收集的是function的名字。
检查props\emits引用是否有问题#
// 5. check useOptions args to make sure it doesn't reference setup scope
// variables
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS)
function checkInvalidScopeReference(node: Node | undefined, method: string) {
if (!node) return
walkIdentifiers(node, id => {
if (setupBindings[id.name]) {
error(
`\`${method}()\` in <script setup> cannot reference locally ` +
`declared variables because it will be hoisted outside of the ` +
`setup() function. If your component options require initialization ` +
`in the module scope, use a separate normal <script> to export ` +
`the options instead.`,
id
)
}
})
}这个报错相信各位学vue3.x的时候都遇到过,比如以下的写法
<script setup>
const defaultPropsValue = '123';
const props = defineProps({ name: { type:String; default: defaultPropsValue } });
</script> 这个时候就会报错了,那么为什么会有这报错?看着报错内容其实就能知道这一段在干嘛了。
it will be hoisted outside of the setup() function. 它(defineProps)将被提升到setup函数之外,而引用的变量还是在setup函数内,自然就有作用域问题。
实际上props的定义会被迁移到和setup函数同级,参考vue2的props定义。
那么这个时候要怎么解决这个问题呢?
有两种方式:
- 根据报错给的方案,将变量放到
normal script block中,也就是我常说的non setup block,为什么这样就可以呢?因为normal script block里的东西会被放到代码头,也就是说是script中的最外层作用域。 - 采用
import的方式引入,import导入的也是会被放到代码头,作用域自然包含了setup函数。
walkIdentifiers这个方法就不看代码了,由于后面还会用到,所以简单的说下做了什么,过一遍传入的节点里的所有子节点,判断他们是否引用了本地(local)变量,然后执行回调,在这个defineProps的场景中执行的回调则是上面说的那个报错。
上面场景同样适用于props解构的变量、props使用type-only的默认值以及emits。
移除non setup block的代码#
// 6. remove non-script content
if (script) {
if (startOffset < scriptStartOffset!) {
// <script setup> before <script>
s.remove(0, startOffset)
s.remove(endOffset, scriptStartOffset!)
s.remove(scriptEndOffset!, source.length)
} else {
// <script> before <script setup>
s.remove(0, scriptStartOffset!)
s.remove(scriptEndOffset!, startOffset)
s.remove(endOffset, source.length)
}
} else {
// only <script setup>
s.remove(0, startOffset)
s.remove(endOffset, source.length)
}前面以及处理完毕setup block和non setup block了,那么这里就把这块non setup script block移除,因为后面是会合并到一起导出的。
分析绑定的元数据(metaData)#
// 7. analyze binding metadata
if (scriptAst) {
Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst.body))
}
if (propsRuntimeDecl) {
for (const key of getObjectOrArrayExpressionKeys(propsRuntimeDecl)) {
bindingMetadata[key] = BindingTypes.PROPS
}
}
for (const key in typeDeclaredProps) {
bindingMetadata[key] = BindingTypes.PROPS
}
// props aliases
if (propsDestructureDecl) {
if (propsDestructureRestId) {
bindingMetadata[propsDestructureRestId] =
BindingTypes.SETUP_REACTIVE_CONST
}
for (const key in propsDestructuredBindings) {
const { local } = propsDestructuredBindings[key]
if (local !== key) {
bindingMetadata[local] = BindingTypes.PROPS_ALIASED
;(bindingMetadata.__propsAliases ||
(bindingMetadata.__propsAliases = {}))[local] = key
}
}
}
for (const [key, { isType, imported, source }] of Object.entries(
userImports
)) {
if (isType) continue
bindingMetadata[key] =
imported === '*' ||
(imported === 'default' && source.endsWith('.vue')) ||
source === 'vue'
? BindingTypes.SETUP_CONST
: BindingTypes.SETUP_MAYBE_REF
}
for (const key in scriptBindings) {
bindingMetadata[key] = scriptBindings[key]
}
for (const key in setupBindings) {
bindingMetadata[key] = setupBindings[key]
}
// known ref bindings
if (refBindings) {
for (const key of refBindings) {
bindingMetadata[key] = BindingTypes.SETUP_REF
}
}analyzeScriptBindings: 这个方法之前分析non script block的时候说到过了,简单的说就是在分析ast节点,给他们定义为某个类型getObjectOrArrayExpressionKeys: 这个方法之前也说过了,看名字就知道是在获取对象/数组表达式的key。

typeDeclaredProps:defineProps的type-only场景获取到的类型定义数据propsDestructureDecl: 说了好几次了,defineProps赋值解构的那个节点propsDestructureRestId: 这个前面貌似没有说过,估计都忘了,这个是defineProps解构赋值中的拓展运算符...。


propsDestructuredBindings: 解构赋值时的本地(local)变量和默认赋值,比如:const { a: _a = 123 } = xxx。userImports: 也是老面孔,存放所有(包括non setup block)import的变量,注意这里的key是local的,而真名是在userImports[xx].imported这个变量中isType指的是这个导入的变量是否是类型,ts中的import type xxx from xxx;source:引入的路径refBindings: 同样是老面孔,ref赋值对象,有一点需要注意的是这里面是包括了使用语法糖$ref的,毕竟只是语法糖,编译完还是长一样的。
这一大段看着挺长,实际上做的事情非常简单,就是在将前面的数据分门别类,组装到bindingsMetadata中。
来看下最终长啥样

注入css变量#
// 8. inject `useCssVars` calls
if (
cssVars.length &&
// no need to do this when targeting SSR
!(options.inlineTemplate && options.templateOptions?.ssr)
) {
helperImports.add(CSS_VARS_HELPER)
helperImports.add('unref')
s.prependRight(
startOffset,
`\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n`
)
} genCssVarsCode: 这个方法其实之前讲过了,可以去看下这篇,这里就不多说了。
坏蛋Dan:vue/compiler-sfc源码分析学习--part2:如何处理script--day1
这一段很简单,就是将css变量注入到js代码中执行,不过需要注意的是这里的css变量转换出来的js其实是和scopedId关联的,因为得用在css中。 这段代码会在组件创建时调用,至于css中如何使用,我们到时候分析compileCss的时候再说。


处理最终的setup参数#
// 9. finalize setup() argument signature
let args = `__props`
if (propsTypeDecl) {
// mark as any and only cast on assignment
// since the user defined complex types may be incompatible with the
// inferred type from generated runtime declarations
args += `: any`
}
// inject user assignment of props
// we use a default __props so that template expressions referencing props
// can use it directly
if (propsIdentifier) {
s.prependLeft(
startOffset,
`\nconst ${propsIdentifier} = __props${
propsTypeDecl ? ` as ${genSetupPropsType(propsTypeDecl)}` : ``
}\n`
)
}
if (propsDestructureRestId) {
s.prependLeft(
startOffset,
`\nconst ${propsDestructureRestId} = ${helper(
`createPropsRestProxy`
)}(__props, ${JSON.stringify(Object.keys(propsDestructuredBindings))})\n`
)
}
// inject temp variables for async context preservation
if (hasAwait) {
const any = isTS ? `: any` : ``
s.prependLeft(startOffset, `\nlet __temp${any}, __restore${any}\n`)
}
const destructureElements =
hasDefineExposeCall || !options.inlineTemplate ? [`expose`] : []
if (emitIdentifier) {
destructureElements.push(
emitIdentifier === `emit` ? `emit` : `emit: ${emitIdentifier}`
)
}
if (destructureElements.length) {
args += `, { ${destructureElements.join(', ')} }`
if (emitsTypeDecl) {
args += `: { emit: (${scriptSetup.content.slice(
emitsTypeDecl.start!,
emitsTypeDecl.end!
)}), expose: any, slots: any, attrs: any }`
}
} propsTypeDecl: 这个老面孔,是only-type的类型定义propsIdentifier: 如果defineProps赋值的对象不是用的解构赋值,那么就能确定是普通变量赋值,比如const a = defineProps({});这个a即是这个propsIndentifiergenSetupPropsType: 代码就不看了,简单的说就是用来处理only-type这种类型定义的,将ts的类型转换成能被解析的runtime类型,毕竟ts并不是能被浏览器识别的代码。helper:
function helper(key: string): string {
helperImports.add(key)
return `_${key}`
} 将辅助函数存储在helperImports中,之前async以及$ref语法糖的辅助函数都在这。

hasAwait:之前的文章里有说到过,如果你直接在setup block中直接用await是没有问题的,因为编译的时候会帮你转换成立即执行函数,这个hasAwait表示是否有使用await。
坏蛋Dan:vue/compiler-sfc源码分析学习--part2:如何处理script--day3
而这里插入的两个变量__temp和__restore就是这个立即执行函数需要的变量。

hasDefineExposeCall: 是否调用了defineExpose将需要暴露出去的变量暴露出去,defineExpose被转换成runtime中的expose,前面的文章中有说到,这里就不多说了。emitIdentifier: 和props的一样,这里自然就是defineEmits赋值的变量,和props不同,defineEmits不能解构,所以这里只能是常规变量赋值。emitsTypeDecl:defineEmits的only-type
这一大段看着也挺长,实际上做的事情很简单,就是在组装setup需要的参数,这里不是指setup语法糖,而是非语法糖的setup方法的参数, props/emits/expose。 当然,还有给await加临时变量等其它的。
这里面你也可以看出来了,props、emit是和setup函数相关的,需要特殊处理,至于要怎么用,后面会有分析。

总结#
今天(好几天)我们主要了解的都是如何处理props、emits相关的,当然,主要都是props的,因为它的场景有三种
typescript下使用type-only加上withDefaults- 使用
runtime type(也就是我们经常写的,直接传给defineProps),变量赋值时是用的解构赋值 - 场景二中赋值是常规赋值,也就是直接赋值给一个变量
其中场景二又有些小场景判断,比如有默认值,有...拓展运算符,有本地命名(local)。
其它#
reactivityTransform处理props#
这里只贴我们需要的代码
const bindingType = scope[id.name]
if (bindingType) {
const isProp = bindingType === 'prop'
if (isStaticProperty(parent) && parent.shorthand) {
// let binding used in a property shorthand
// skip for destructure patterns
if (
!(parent as any).inPattern ||
isInDestructureAssignment(parent, parentStack)
) {
if (isProp) {
if (escapeScope) {
// prop binding in $$()
// { prop } -> { prop: __props_prop }
registerEscapedPropBinding(id)
s.appendLeft(
id.end! + offset,
`: __props_${propsLocalToPublicMap[id.name]}`
)
} else {
// { prop } -> { prop: __props.prop }
s.appendLeft(
id.end! + offset,
`: ${genPropsAccessExp(propsLocalToPublicMap[id.name])}`
)
}
} else {
// { foo } -> { foo: foo.value }
s.appendLeft(id.end! + offset, `: ${id.name}.value`)
}
}
} else {
if (isProp) {
if (escapeScope) {
// x --> __props_x
registerEscapedPropBinding(id)
s.overwrite(
id.start! + offset,
id.end! + offset,
`__props_${propsLocalToPublicMap[id.name]}`
)
} else {
// x --> __props.x
s.overwrite(
id.start! + offset,
id.end! + offset,
genPropsAccessExp(propsLocalToPublicMap[id.name])
)
}
} else {
// x --> x.value
s.appendLeft(id.end! + offset, '.value')
}
}
}isStaticProperty: 一个静态属性,类型属于正常类型,最重要的是computed不能为true,为true则表明这个node被重写了setter以及getter。
export const isStaticProperty = (node: Node): node is ObjectProperty =>
node &&
(node.type === 'ObjectProperty' || node.type === 'ObjectMethod') &&
!node.computed如果你是通过解构的方式获取到props,那么这里会转化成__props.xxx。比如const { a } from defineProps({}); 这里的a会变成__props.a。
这里还有一段代码能解释我们的$ref语法糖: s.appendLeft(id.end!+ offset,'.value'),再加上importHelper也就是import { ref as _ref } from 'vue';所以编译出来后和你使用的xxx.value效果是一致的。
参考#
- ^vue3-defineProps-only-type https://vuejs.org/api/sfc-script-setup.html#typescript-only-features
- ^vue3-defineEmits-only-type https://vuejs.org/api/sfc-script-setup.html#typescript-only-features
- ^babel-TSInterfaceBody https://babeljs.io/docs/en/babel-types#tsinterfacebody
- ^babel-TSTypeAnnotation https://babeljs.io/docs/en/babel-types#tstypeannotation
- ^babel-TSLiteralType https://babeljs.io/docs/en/babel-types#tsliteraltype
- ^babel-TSUnionType https://babeljs.io/docs/en/babel-types#tsuniontype
编辑于 2022-12-05 16:37・IP 属地广东
