上篇文章中我们分析了如何处理非setup
语法糖script block
,知道了是如何处理分析import
、export
以及各种变量的。如果没看过可以去看下。
坏蛋Dan:vue/compiler-sfc源码分析学习--part2:如何处理script--day2
点个赞也是可以滴~doge
老规矩会省略error
场景以及一些无关代码,另外setup script block
或者setup block
指的是<script setup>...</script>
,同样non setup block
指的是<script>...</script>
。
话不多说,我们接着往下分析。
还是沿用上期的那个~
setup block
处理成ast
plugins
: 之前说过了,typescript、jsx、tsx
或者babel
自己的plugin
等。sourceType
: Indicate the mode the code should be parsed in. Files with ES6 imports and exports are considered "module"
[1],之前学babel
[2]的文章里应该有提到过,简单的说就是通知parser
如何去处理源码,module
自然是模块,将源码当作模块去处理。_parse
: @babel/parser
[3]的parse
,不多说。这段代码很简单,就是处理setup script block
,转换为ast
。
setupscript
的ast
前面处理成ast
了,这里自然就是分析。
代码有些长,300来行,这回我们就不直接贴出来了,换下展示效果,先展示整体,再一小块一小块分析。
整体是for
循环遍历scriptSetupAst
startOffset
: setupscript``block
代码的起始位置。校正代码起始结束位置
trailingComments
: 注释一类中的末尾注释,类似的还有头部注释(leadingComments
),babel
不会忽略他们。这块代码很简单,就是在校正代码的起始结束位置,将位于代码下面的comments
也就是注释行数列数也算进去。我们接着往下看。
清理代码间空白区域
这个很简单,就是end
位置继续往下,知道没有空的space
结束,在我们parse
之前会对源码做trim()
处理,但是这仅能去除前后空白,并且不能做全局匹配去除,因为会误伤,所以这里又做了去除空白的处理。
接着往下看。
import
这块代码有些小长
imported
: 这个就不多说了,上篇文章说过了,这个imported
指的是import
的对象,比如import { a } from 'b'
中的a
removeSpecifier
: 代码就在上面,看名字就知道是用来移除这个字段的,需要注意,操作的对象是s
,也就是通过magic-string
[4]处理过后的。registerUserImport
: 也是熟人了,上篇文章也说过了,这里就不多说了,简单的说就是将import
目标注册到userImport
这个集合中。这段代码有点小长,但是很简单。如果你看过上篇文章,应该就没有阅读难点了。简单的说就是和non script block
一样的操作,都是将import
的对象存放到userImport
这个集合中。不过setup script block
有额外的操作。
defineProps、defineEmits、defineExpose
三个api
的导入代码,为什么要这么做呢?因为在setup block
中不需要手动导入了,可以直接用。import
的家伙之前是否已经import
过了,有了直接移除。是的,你的non setup script block
的import
其实是共用的,这也就是为什么non setup block
要被移到setup block
之前。 当然这里还有一个问题就是别名(alias
)问题。这里注册到userImport
的key
就是这个alias
,而多个import
可能出现别名一样但是导入的东西和路径不一样的情况。比如以下场景这就存在问题了,所以这里还多了一层对路径以及导入对象的判断,如果确定没问题则移除重复的,如果相同别名而又不同内容则直接叉出去报错。
来看下数据
userImportAlias
:这个是专门用来收集vue
自己的api
的,因为允许别名(alias
),所以还得收集下才行,不然到时候自己都不认识了。userImportAlias[imported] = local
,其中imported
是api
原名,而local
是别名,没有别名用原名。可以很清楚的看到里面不仅有当前script block
的import
,还有之前的non setup block
的import
。
我们接着往下看
defineProps、defineEmits、withDefaults、defineExpose
表达式调用processDefineProps
: 这里不贴代码,下面开个小标题来分析下,简单的说就是在处理props
,收集变量啊啥的,具体看下面**processDefineProps
做了什么**这一个小section
。processDefineEmits
: 同上,收集emits
赋值的变量名。processWithDefaults
: 同上,收集赋值给props
的数据。processDefineExpose
: 不做收集,仅是判断。calle
: 调用表达式中被调用的对象,比如defineProps({})
,defineProps
就是被调用的对象。这段代码很简单,这个小标题中的代码都是处理普通表达式的,比如defineProps({xxx})
这样,并没有声明变量等,所以其实这里并没有做收集处理(WithDefaults
除外,这货收集了)。 只是在做判断,是否正常调用api
。
来看下代码,有点小长
isCallOf
: 这个上篇文章也说过了,这里就不多说了。hasDefinePropsCall
: 不多说,一个script block
中只能有一个defineProps
。propsRuntimeDecl
: node.arguments[0]
就是你传给defineProps
的params
。node.typeParameters
: 这个是typescript
代码解析后特有的类型定义,为什么这里会有呢?因为vue3.x
支持defineProps
的时候通过type-only
的方式定义props
类型[5],这样就不需要runtime
代码定义,比如:当然这得是在typescript
下才能使用,另外如果用了type-only
就不能再使用runtime define
了,也就是常规的props
定义。两个之间只能有一个。
propsTypeDeclRaw
: type-only
定义的具体。propsTypeDecl
: propsTypeDeclaration
, type
为TSTypeLiteral
[6]的那个节点。resolveQualifiedType
: 代码就不看了,简单的说就是找到你这个type
,由于你这个type
可能有好几种方式,比如import
进来的又或者是通过interface
传给的,所以得找到这个type
。declId
: 这个是声明目标的对象,比如const a = defineProps({})
,这个a
就是这个对象,也是这个表达式的id
节点。enablePropsTransform
: 其实就是reactivityTransform
[7],前面也说过很多次了,这里就不多说了。ObjectPattern
[8]: 这个之前也说过了,是解构赋值,比如const { a, b, c } = {xxx};
,这个a,b,c
就是解构赋值的对象变量。ObjectProperty
[9]: 对象属性,这个就不多说了,大家应该都清楚,不过需要注意的一点是,并非所有的放到对象上的东西都是objectProperty
,比如这个{ log () {} }
,这个log
就不是objectProperty
,而是objectMethod
[10] 。这些也是前面文章中就说到过的,大家可以去看下。StringLiteral
[11]: 字符串节点AssignmentPattern
[12]: 解构的默认赋值 ,比如const { a = 123 } = xxx
,这个123
就是默认赋值。left,right
: 解构赋值的左右边当把所有不明白的点都罗列出来时这一大段代码就变得非常简单了~
总结一下processDefineProps
做了什么
defineProps
,不是直接叉出去 ;defineProps
了,是的话直接叉出去并且报错;type-only
定义,如果是的话接着判断是否又传入了runtime
定义,是的话直接叉出去,一处定义只允许有一种。找到type-only
的目标类型定义,如果没找到直接报错。declId
也就是变量声明左边的家伙,是的话进入以下场景: 是否开启了reactivityTransform并且是解构赋值 。propsIdentifier
字段中。propsDestructuredBindings
集合中,变量名字包括默认值。稍微调整下我们的测试单元,看下数据是怎么样的。
在分析看代码之前,我们先稍微修改下我们的测试单元,毕竟之前并没有加上defineEmits
这个没啥好说的。。。该分析的东西processDefineProps
中都分析完了。
简单的说和processDefineProps
做的事情一样,defineEmits
也是支持only-type
[13]的,当然,也是在typescript
的情况下,也只能二选一。
另外defineEmits
不支持解构,所以直接就一个字段emitIdentifier
存储了。
withDefaults
方法是用来处理defineProps
使用only-type
时无法设置默认值的问题的。
但其实,还有另一种方式——解构赋值,通过给解构的变量设置默认值的方式来实现给props
赋值默认值,没想到吧。
这块代码很简单,判断是否符合执行withDefaults
的条件,如果确实是用了only-type
但是使用了解构赋值,这时就没必要执行这个方法,直接叉出去。如果符合条件则放到propsRuntimeDefaults
变量中。
defineExpose
[14] ,这个api
有些陌生,因为是vue3
特有的,vue2.x
中通过$refs
或者$parents
等获取组件的数据或者方法时是可以直接获取的。但是这在3.x
中就不适用,默认是没有东西暴露的。所以这里就需要这个defineExpose
抛出你想暴露的数据方法,这样就非常安全。话不多说,来看下代码。
没了。。
这里没做收集,只是判断是否是这个api
并且是否重复使用而已。
props、emits、withDefaults
数据小标题5中我们提到了收集,但实际上收集执行的代码是在这里。比如以下情况会执行这段代码:
变量赋值表达式(变量声明,VariableDeclaration
)的情况下会执行这段代码。
这段代码也没什么好说的,收集变量名和数据,然后移除这段代码,毕竟不是runtime
的代码。
另外还有一点需要注意,那就是这里仅收集props、emit
的数据。
这个不多说,上篇文章说过walkDeclaration
这个方法了,可以去看下。这里简单的说就是收集变量等并分门别类标记,最后存放到bindings
中。不过需要注意这个bindings
并不是共用的,也就是说non setup block
和setup block
各自有各自的bindings
集合。
这里需要注意,收集的是除props、emits
以外的,毕竟上面已经处理过了。
至于为什么要收集呢?因为语法糖中的东西默认最终都会setup()
出去。
await
walk
: 这个方法来自estree-walker
[15],这个包简单的说就是可以帮你移动/删除/替换ast
里的节点。不过ast
的方案存在多种,这里支持ESTree-compliant AST
。它的使用方式如下参数第一个传入需要处理的ast
,第二个则是回调。其中enter
表示进入某个节点,leave
自然就是离开。
而如果不想处理某个节点可以执行this.skip()
跳过。需要替换则执行this.replace(newNode)
。移除则是this.remove()
。
扯远了,我们继续看代码。
scope
: 这是一个栈空间,这种嵌套的用栈来处理挺不错的。BlockStatement
[16]: 块语句,也就是一个块作用域。比如{ function a () {} }
,这个就是一个块语句节点。AwaitExpression
[17]: 这个不用多说,await
表达式。由于我们的代码中并没有await
,所以我们加点料。需要注意的一点是setup block
中实际上是允许你直接使用await
的,不需要用async
包裹,为什么可以呢,我们等会会分析到。
processAwait
: 我们来看下代码我把注释带上之后应该就已经一目了然了~
withAsyncContext
: 存放在另一个包中,毕竟是runtime
来的,具体@vue/runtime-core/src/apiSetupHelpers.ts
[18]中,具体分析请看其它-withAsyncContext
这一小标题。简单的说就是帮你重写这段代码,用立即执行函数包裹,这也就是为什么你写await
不需要async
包裹的原因,编译时自动帮你处理了。这里有个点需要注意,立即执行函数前最好带一个;
,不带的话可能会出现解析出错。
总结一下迁移代码以及处理await
这段代码
这段代码做的事情很简单,就是在把变量声明等迁移到文件头,而如果是await
的话顺便将await
用立即执行函数包裹 。
如果你用了typescript
,这里会收集ts
的类型,然后移除。毕竟类型定义不是我们runtime
需要的(enum
除外,用得上)。
ok
,今天就分析到这里。别忘了我们分析的范围,在一个for
循环中,遍历的setupscript
的节点。
主要做了俩事:
setup block
处理成ast
ast
其中遍历节点过程中又做了几件事
import
变量。defineProps、defineEmits、withDefaults、defineExpose
这几个api
相关的代码,如果是变量声明,那就收集变量以及数据(withDefaults
仅收集数据,defineExpose
改写为expose
,是个runtime
方法)。await
表达式场景。typescript
,收集类型定义等并迁移至代码外部。具体请往回看。
上面分析如何处理await
时并没有分析这个方法,仅说了整体是怎么样的,并没有说到这里面具体做了什么,立即执行函数包裹我们就不多说了,我们来看下这个runtime
方法是干啥用的。
getCurrentInstance
: 代码就不看了,获取当前上下文环境,一般都是组件自身getAwaitable
: 你的await
目标,不过被包裹了一层,变成了回调。unsetCurrentInstance
: 当前作用域中移除了这货并且注销了他。isPromise
: 代码挺有意思的setCurrentInstance
: 注册回来。结合demo
分析
withAsyncContext
这个方法中返回了一个数组,第一个数是加了个catch
的promise
,也即是你的await
目标,但是多了层catch
监听。而第二个数__restore
则是一个回调,将当前上下文环境重新注册回来。
这样也保证了组件的顺序是正确的。
如果觉得这篇文章对你有帮助,麻烦点个赞,谢谢!
发布于 2022-11-25 12:54・IP 属地广东