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

前言

上篇文章中我们分析了是如何处理setup blockast的,这篇文章我们接着往下分析,如果没看过可以去看下~。

坏蛋Dan:vue/compiler-sfc源码分析学习--part2:如何处理script--day3


正文

老规矩我们说的non setup block或者non setup script block指的是<script>...</script>这样,反之则是<script setup>...</script>

测试单元

老样子

收集ref语法糖变量

这个就不多说了,前面的文章也说过了。就是收集使用了$ref这种语法糖的变量,不过这里还有一个点需要看下,propsDestructuredBindings把这东西也传进去了。propsDestructuredBindings: 是之前我们收集到的props解构声明变量集合。把这个东西传进去可能是有什么操作需要,所以我们需要分析下里面这里做了什么才行。分析放其它中,因为不是这个包中的代码。这里简单的说下即可,简单的说就是和处理$ref一样,需要将解构的prop处理成__prop.xxx,因为这样才不会失去响应式。


处理并收集runtime props

  • propsTypeDecl: 上篇文章中我们在分析processDefineProps时有提到过,这个变量是针对typescriptonly-type[1]场景的,存放的是propsts类型声明的节点,只要是ts的东西,除了enum,几乎都得在编译的时候就处理掉。
  • typeDeclaredProps: props的类型声明的集合,此时这里还是空的。
  • declaredProps: 上篇末尾有提到的,如果是typescript下的会进行收集类型定义并存放到这个变量中,然后移除源码中相关的代码,毕竟浏览器不认识ts代码。
  • isProd: 是否是生产环境。
  • interRuntimeType: 看下代码

看名字就知道,这是用来定义runtime时的类型,typescript的类型都会被转换为浏览器知道的类型。

这一个小标题的代码很好理解,就是收集props类型声明,也就是上面说的typeDeclaredProps并统一格式为{ key, required, type }


处理并收集runtime emits

我把调用的方法都放进来了,代码有些长,但是挺好理解的。

  • emitsTypeDecl: 这里自然指的是被收集的emitsonly-type[2]类型声明的节点
  • TSInterfaceBody[3]: 看名字就知道是typescriptinterface类型声明的主体。
  • TSCallSignatureDeclaration: 调用函数声明。。这是我个人的翻译。。比如这样的
  • TSTypeAnnotation[4]: 函数参数类型声明,就是上边这个number/string
  • TSLiteralType[5]: 比如这种type A = "test"literal指的是字面的,也就是说写死的数字\字符串等都符合这个类型。
  • TSUnionType[6]: 比如这种 a: number | string

其他类型之前都讲过了,这里就不多赘述。

这个小标题也挺简单的, 和上面一样,也是在处理并收集emit的类型定义,不过这里收集的是function的名字。


检查props\emits引用是否有问题

这个报错相信各位学vue3.x的时候都遇到过,比如以下的写法

这个时候就会报错了,那么为什么会有这报错?看着报错内容其实就能知道这一段在干嘛了。

it will be hoisted outside of the setup() function. 它(defineProps)将被提升到setup函数之外,而引用的变量还是在setup函数内,自然就有作用域问题。

实际上props的定义会被迁移到和setup函数同级,参考vue2props定义。

那么这个时候要怎么解决这个问题呢?

有两种方式:

  1. 根据报错给的方案,将变量放到normal script block中,也就是我常说的non setup block,为什么这样就可以呢?因为normal script block里的东西会被放到代码头,也就是说是script中的最外层作用域。
  2. 采用import的方式引入,import导入的也是会被放到代码头,作用域自然包含了setup函数。

walkIdentifiers这个方法就不看代码了,由于后面还会用到,所以简单的说下做了什么,过一遍传入的节点里的所有子节点,判断他们是否引用了本地(local)变量,然后执行回调,在这个defineProps的场景中执行的回调则是上面说的那个报错。

上面场景同样适用于props解构的变量、props使用type-only的默认值以及emits


移除non setup block的代码

前面以及处理完毕setup blocknon setup block了,那么这里就把这块non setup script block移除,因为后面是会合并到一起导出的。


分析绑定的元数据(metaData)

  • analyzeScriptBindings: 这个方法之前分析non script block的时候说到过了,简单的说就是在分析ast节点,给他们定义为某个类型
  • getObjectOrArrayExpressionKeys: 这个方法之前也说过了,看名字就知道是在获取对象/数组表达式的key

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

  • propsDestructuredBindings: 解构赋值时的本地(local)变量和默认赋值,比如: const { a: _a = 123 } = xxx
  • userImports: 也是老面孔,存放所有(包括non setup block)import的变量,注意这里的keylocal的,而真名是在userImports[xx].imported这个变量中
  • isType指的是这个导入的变量是否是类型,ts中的import type xxx from xxx
  • source:引入的路径
  • refBindings: 同样是老面孔,ref赋值对象,有一点需要注意的是这里面是包括了使用语法糖$ref的,毕竟只是语法糖,编译完还是长一样的。

这一大段看着挺长,实际上做的事情非常简单,就是在将前面的数据分门别类,组装到bindingsMetadata中。

来看下最终长啥样


注入css变量

  • genCssVarsCode: 这个方法其实之前讲过了,可以去看下这篇,这里就不多说了。

坏蛋Dan:vue/compiler-sfc源码分析学习--part2:如何处理script--day1

这一段很简单,就是将css变量注入到js代码中执行,不过需要注意的是这里的css变量转换出来的js其实是和scopedId关联的,因为得用在css中。 这段代码会在组件创建时调用,至于css中如何使用,我们到时候分析compileCss的时候再说。


处理最终的setup参数

  • propsTypeDecl: 这个老面孔,是only-type的类型定义
  • propsIdentifier: 如果defineProps赋值的对象不是用的解构赋值,那么就能确定是普通变量赋值,比如const a = defineProps({});这个a即是这个propsIndentifier
  • genSetupPropsType: 代码就不看了,简单的说就是用来处理only-type这种类型定义的,将ts的类型转换成能被解析的runtime类型,毕竟ts并不是能被浏览器识别的代码。
  • helper

将辅助函数存储在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: defineEmitsonly-type

这一大段看着也挺长,实际上做的事情很简单,就是在组装setup需要的参数,这里不是指setup语法糖,而是非语法糖的setup方法的参数, props/emits/expose。 当然,还有给await加临时变量等其它的。

这里面你也可以看出来了,props、emit是和setup函数相关的,需要特殊处理,至于要怎么用,后面会有分析。


总结

今天(好几天)我们主要了解的都是如何处理props、emits相关的,当然,主要都是props的,因为它的场景有三种

  1. typescript下使用type-only加上withDefaults
  2. 使用runtime type(也就是我们经常写的,直接传给defineProps),变量赋值时是用的解构赋值
  3. 场景二中赋值是常规赋值,也就是直接赋值给一个变量

其中场景二又有些小场景判断,比如有默认值,有...拓展运算符,有本地命名(local)。


其它

reactivityTransform处理props

这里只贴我们需要的代码

  • isStaticProperty: 一个静态属性,类型属于正常类型,最重要的是computed不能为true,为true则表明这个node被重写了setter以及getter

如果你是通过解构的方式获取到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效果是一致的。

参考

  1. ^vue3-defineProps-only-type https://vuejs.org/api/sfc-script-setup.html#typescript-only-features
  2. ^vue3-defineEmits-only-type https://vuejs.org/api/sfc-script-setup.html#typescript-only-features
  3. ^babel-TSInterfaceBody https://babeljs.io/docs/en/babel-types#tsinterfacebody
  4. ^babel-TSTypeAnnotation https://babeljs.io/docs/en/babel-types#tstypeannotation
  5. ^babel-TSLiteralType https://babeljs.io/docs/en/babel-types#tsliteraltype
  6. ^babel-TSUnionType https://babeljs.io/docs/en/babel-types#tsuniontype

编辑于 2022-12-05 16:37・IP 属地广东