坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.4

前言

上篇文章中我们分析了如何处理非setup语法糖script block,知道了是如何处理分析importexport以及各种变量的。如果没看过可以去看下。

坏蛋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


遍历分析setupscriptast

前面处理成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有额外的操作。

  1. 移除defineProps、defineEmits、defineExpose三个api的导入代码,为什么要这么做呢?因为在setup block中不需要手动导入了,可以直接用。
  2. 判断import的家伙之前是否已经import过了,有了直接移除。是的,你的non setup script blockimport其实是共用的,这也就是为什么non setup block要被移到setup block之前。 当然这里还有一个问题就是别名(alias)问题。这里注册到userImportkey就是这个alias,而多个import可能出现别名一样但是导入的东西和路径不一样的情况。比如以下场景

这就存在问题了,所以这里还多了一层对路径以及导入对象的判断,如果确定没问题则移除重复的,如果相同别名而又不同内容则直接叉出去报错。

来看下数据

  • userImportAlias:这个是专门用来收集vue自己的api的,因为允许别名(alias),所以还得收集下才行,不然到时候自己都不认识了。userImportAlias[imported] = local,其中importedapi原名,而local是别名,没有别名用原名。

可以很清楚的看到里面不仅有当前script blockimport,还有之前的non setup blockimport

我们接着往下看


处理defineProps、defineEmits、withDefaults、defineExpose表达式调用

  • processDefineProps: 这里不贴代码,下面开个小标题来分析下,简单的说就是在处理props,收集变量啊啥的,具体看下面**processDefineProps做了什么**这一个小section
  • processDefineEmits: 同上,收集emits赋值的变量名。
  • processWithDefaults: 同上,收集赋值给props的数据。
  • processDefineExpose: 不做收集,仅是判断。
  • calle: 调用表达式中被调用的对象,比如defineProps({})defineProps就是被调用的对象。

这段代码很简单,这个小标题中的代码都是处理普通表达式的,比如defineProps({xxx})这样,并没有声明变量等,所以其实这里并没有做收集处理(WithDefaults除外,这货收集了)。 只是在做判断,是否正常调用api


processDefineProps做了什么

来看下代码,有点小长

  • isCallOf: 这个上篇文章也说过了,这里就不多说了。
  • hasDefinePropsCall: 不多说,一个script block中只能有一个defineProps
  • propsRuntimeDecl: node.arguments[0]就是你传给definePropsparams
  • node.typeParameters: 这个是typescript代码解析后特有的类型定义,为什么这里会有呢?因为vue3.x支持defineProps的时候通过type-only的方式定义props类型[5],这样就不需要runtime代码定义,比如:

当然这得是在typescript下才能使用,另外如果用了type-only就不能再使用runtime define了,也就是常规的props定义。两个之间只能有一个。

  • propsTypeDeclRaw: type-only定义的具体。
  • propsTypeDecl: propsTypeDeclaration, typeTSTypeLiteral[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做了什么

  1. 先判断你是否是defineProps,不是直接叉出去 ;
  2. 判断之前是否已经有调用过defineProps了,是的话直接叉出去并且报错;
  3. 判断是否是type-only定义,如果是的话接着判断是否又传入了runtime定义,是的话直接叉出去,一处定义只允许有一种。找到type-only的目标类型定义,如果没找到直接报错。
  4. 判断是否传入了declId也就是变量声明左边的家伙,是的话进入以下场景: 是否开启了reactivityTransform并且是解构赋值 。
  • 不是:直接截取这个变量名存入propsIdentifier字段中。
  • 是:遍历这个解构赋值中每一个变量,存入propsDestructuredBindings集合中,变量名字包括默认值。

稍微调整下我们的测试单元,看下数据是怎么样的。


processDefineEmits做了什么

在分析看代码之前,我们先稍微修改下我们的测试单元,毕竟之前并没有加上defineEmits

这个没啥好说的。。。该分析的东西processDefineProps中都分析完了。

简单的说和processDefineProps做的事情一样,defineEmits也是支持only-type[13]的,当然,也是在typescript的情况下,也只能二选一。

另外defineEmits不支持解构,所以直接就一个字段emitIdentifier存储了。


processWithDefaults做了什么

withDefaults方法是用来处理defineProps使用only-type时无法设置默认值的问题的。

但其实,还有另一种方式——解构赋值,通过给解构的变量设置默认值的方式来实现给props赋值默认值,没想到吧。

这块代码很简单,判断是否符合执行withDefaults的条件,如果确实是用了only-type但是使用了解构赋值,这时就没必要执行这个方法,直接叉出去。如果符合条件则放到propsRuntimeDefaults变量中。


processDefineExpose做了什么

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 blocksetup 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用立即执行函数包裹 。


处理TS代码

如果你用了typescript,这里会收集ts的类型,然后移除。毕竟类型定义不是我们runtime需要的(enum除外,用得上)。


总结

ok,今天就分析到这里。别忘了我们分析的范围,在一个for循环中,遍历的setupscript的节点。

主要做了俩事:

  1. setup block处理成ast
  2. 遍历ast

其中遍历节点过程中又做了几件事

  1. 处理以及收集import变量。
  2. 处理defineProps、defineEmits、withDefaults、defineExpose这几个api相关的代码,如果是变量声明,那就收集变量以及数据(withDefaults仅收集数据,defineExpose改写为expose,是个runtime方法)。
  3. 收集变量声明、函数声明以及类声明。
  4. 迁移变量声明等至代码头,并且处理await表达式场景。
  5. 如果是typescript,收集类型定义等并迁移至代码外部。

具体请往回看。


其他

withAsyncContext

上面分析如何处理await时并没有分析这个方法,仅说了整体是怎么样的,并没有说到这里面具体做了什么,立即执行函数包裹我们就不多说了,我们来看下这个runtime方法是干啥用的。

  • getCurrentInstance: 代码就不看了,获取当前上下文环境,一般都是组件自身
  • getAwaitable: 你的await目标,不过被包裹了一层,变成了回调。
  • unsetCurrentInstance: 当前作用域中移除了这货并且注销了他。
  • isPromise: 代码挺有意思的
  • setCurrentInstance: 注册回来。

结合demo分析

withAsyncContext这个方法中返回了一个数组,第一个数是加了个catchpromise,也即是你的await目标,但是多了层catch监听。而第二个数__restore则是一个回调,将当前上下文环境重新注册回来。

这样也保证了组件的顺序是正确的。

如果觉得这篇文章对你有帮助,麻烦点个赞,谢谢!

参考

  1. ^@babel/parser-options https://babeljs.io/docs/en/babel-parser#options
  2. ^babel入门学习 https://zhuanlan.zhihu.com/p/576231528
  3. ^@babel/parser https://babeljs.io/docs/en/babel-parser
  4. ^magic-string https://www.npmjs.com/package/magic-string
  5. ^vue3-type-only-defineProps https://vuejs.org/api/sfc-script-setup.html#typescript-only-features
  6. ^babel-tsTypeLiteral https://babeljs.io/docs/en/babel-types#tstypeliteral
  7. ^vue3-reactivityTransform https://vuejs.org/guide/extras/reactivity-transform.html#reactivity-transform
  8. ^babel-objectPattern https://babeljs.io/docs/en/babel-types#objectpattern
  9. ^babel-objectProperty https://babeljs.io/docs/en/babel-types#objectproperty
  10. ^babel-objectMethod https://babeljs.io/docs/en/babel-types#objectmethod
  11. ^babel-stringLiteral https://babeljs.io/docs/en/babel-types#stringliteral
  12. ^babel-assignmentPattern https://babeljs.io/docs/en/babel-types#assignmentpattern
  13. ^vue3-defineEmits-only-type https://vuejs.org/api/sfc-script-setup.html#typescript-only-features
  14. ^defineExpose https://vuejs.org/api/sfc-script-setup.html#defineexpose
  15. ^estree-walker https://www.npmjs.com/package/estree-walker
  16. ^babel-BlockStatement https://babeljs.io/docs/en/babel-types#blockstatement
  17. ^babel-awaitExpression https://babeljs.io/docs/en/babel-types#awaitexpression
  18. ^@vue/runtime-core https://github.com/vuejs/core/tree/main/packages/runtime-core

发布于 2022-11-25 12:54・IP 属地广东