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

前言

上篇文章我们找到了调用入口,找了测试单元,也分析了非setupscript block做了什么。如果没看过上篇文章的可以去看下。

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

今天我们就接着往下分析。


正文

老规矩,同样是省略一些error判断和无关场景,一下non setup script block指的是不带有任何属性的script block,比如。同理`setup script block`指的是

切换测试单元

由于day1分析时分析到了non setup script block,切换了测试单元,所以在分析代码之前我们需要切换回来。

这个测试元带有一个setup script block以及一个non setup block并且这个单纯的script不带有lang

然后直接左上角debug

处理单纯script block

啥叫单纯script block?指的就是这种,当然,也可以带上`lang`,比如

注意你的script blocklang都需要保持一致。

代码有些长并且也不太好再切块来分析,所以我们只能是慢慢来了。

  • magic-string[1]: 转换为可修改的source源码,上篇文章中提到过了,这里就不多说了。
  • startOffset: setup block代码起始位置。
  • endOffset: setup block代码的结束位置。
  • scriptStartOffset: non setup block代码的起始位置。
  • scriptEndOffset: non setup block代码的结束位置。
  • parse: 来看下parse方法的代码。

其实还是调用babel[2]parse方法。接下来是和babelast有关的代码。

由于我们的non script block并不带有import,所以我们先暂停调试,切回去加上import

然后重新debug

  • ImportDeclaration[3]: import声明
  • ImportSpecifier[4]: 非default,即是通过解构的方式导入的,而default则是:ImportDefaultSpecifier[5]
  • node.importKind === 'type': 这里提一下这个,这个是专门import类型的,用法如下。
  • registerUserImport: 看名字就知道是用来注册你代码里的import的,来看下代码。

注册import

  • source: 对应node.source.value, 导入对象所在的模块
  • local: 对应specifier.local.name: 在当前文件中的namespace,比如{ a as _a },这个_a就是一个local name
  • imported: 如果import的是default,比如import a from 'b.js',则不会有imported这个字段
  • isType: 这指的是导入是否仅限于type,比如import type { a } from 'b.d.ts'这样。
  • isFromSetup: false,毕竟我们在分析一个单纯的script block
  • needTemplateUsageCheck: 这个是针对是否是inlineTemplate的,是的话不需要check
  • isImportUsed: 这个看名字就知道是干啥的,确认你的importlocal name是否被template使用了。代码就不看了,里面是通过ast去处理排查的。

这段代码也挺简单,主要做了仨事

  1. 搜集从vueimportapi,格式为[original name]:namespace
  2. 判断是否需要检查template以及这个import的对象是否被template使用了 。
  3. 注册到userImports中。

让我们回到之前的代码,回到遍历ast节点那里。

elseif(node.type ==='ExportDefaultDeclaration')场景

  • ExportDefaultDeclaration[6]: 这个看名字就知道是export default声明。
  • hasDefaultExportName: 有name字段
  • hasDefaultExportRender: 有render字段

这块代码也很简单,就是判断这个non setup block是否有export default,如果有,判断是否有namerender字段,为啥要判断这俩呢?我们后面会说到。然后将字符串export defaultconst __default__ =

这里你是否有疑惑?前面不是也处理过了吗?为什么这里又有一个这样的,其实前面处理的是没有setup语法糖script block的场景。而这里是存在setup语法糖block的情况下又存在单个script block

我们接着往下看

else if (node.type === 'ExportNamedDeclaration')场景

由于我们的测试单元没有这块逻辑,所以我们加点料

  • ExportNamedDeclaration[7]: 导出声明。

  • defaultSpecifier: 这个指的是导出默认,比如export { c as default } from 'd.js'但是这个需要babel的相关插件处理,不然会报错。
  • walkDeclaration: 这里就不分析了,看下一个小标题即可。简单的说就是在给将被setup暴露出去的数据分门别类。

这段代码也不难理解,主要做了俩事

  1. 判断是否是exportdefault,如果是,先移除这行代码,然后改为import { x as __default__ } from './x'这样,为什么要这么做呢?因为后面会统一export __default__,所以这里自然得改为import
  2. 如果节点有声明(declaration),调用walkDeclaration

我们接着往下分析

其它场景

  • VariableDeclaration[8]: 变量声明。
  • FunctionDeclaration[9]: 函数声明,而且是function xx () {}这种,箭头函数声明则是另一个。
  • ClassDeclaration[10]: 类声明。
  • TSEnumDeclaration: typescript的枚举类型声明,比如:
  • walkDeclaration: 这里也不分析,看下一个小标题即可。简单的说就是在给将被setup暴露出去的数据分门别类。

来总结下这段对ast节点遍历处理。

简单的说就是在从上到下整理代码,具体逻辑请往回看。

  1. 处理import, 将他们注册到userImport这个集合中。
  2. 处理export default,将export default改写为const __default__ =,等着后面统一export
  3. 处理非defaultexport,都被收集到bingdings这个集合中等着后面统一setup暴露出去。当然如果是export { x as default }这种就会被移除,取而代之的是import { x as __default__ } from,毕竟导出得统一导出,你这里直接导出default就乱套了。
  4. 非以上场景中遇到变量声明,函数声明,类声明,TS枚举声明则调用walkDeclaration将变量分门别类收集到bindings这个集合中。

然后让我们回到处理这个non setup script block的代码中,由于里的太远,所以这里就再贴一次代码 。

  • enableReactivityTransform: 这个day1中解释过了,简单的说就是一个目前还在实验中的语法糖,会在编译过程中替换为原来的用法。需要在options中配置reactivityTransform: true
  • shouldTransform: day1中也说过了,判断你这里面有没有语法糖用法,比如$ref
  • transformAST: 同上,这个方法来自@vue/reactivity-transform[11]这个包,另外这个方法里面是会改动源码的,比如$ref语法糖不需要xxx.value = xxx来实现响应式,直接xxx = xxx就行了,而这里面就是帮你把xxx = xxx转换成xxx.value = xxx
  • importedHelpers: 辅助函数,一般runtime才会需要辅助函数。 上篇文章里也说过了,实际上就是帮你导入对应的vueapi。比如你用了$ref,然后这里会帮你import { ref as _ref } from 'vue';
  • rootRefs: 语法糖赋值的对象数组,比如const a = $ref(0),这里的a就会被放到rootRefs中。

这两块代码很简单,就是处理reactivityTransform语法糖还有将non setup script block迁移至setup script block之前,这样non setup script中的__default__才能正常被setup script block调用。

总结一下这一个标题内容

简单的说就是在处理带有setupscript block之前,先处理掉没有setup属性的script block

  1. 先是调用babel将代码转换为ast,通过调用magic-string将代码转换成可操作的source方便后面移除或者替换代码。
  2. 遍历ast
  • 收集import
  • 处理export,具体看上面分析;
  • 收集声明,包括上面的export、变量、函数、类以及typescript的枚举(enum)声明;
  1. 判断是否使用activityTransform语法糖,用了转换成原来的。
  2. non setup script block迁移至setup script block之前,确保__default__能被setup的调用到。

walkDeclaration做了什么。

单独拿出来说,因为里面的逻辑还是比较绕的,涉及到很多babel的东西。

代码有些乱,简单的说一下。

TSEnumDeclaration、 FunctionDeclaration、 ClassDeclaration这几个都会被标记为setup-const

VariableDeclaration 是用于变量声明的,而变量声明是一个不稳定状态,可能被赋值一个常量又可能被赋值一个箭头函数,所以需要特殊处理。

由于我们的测试单元没有设置变量,所以我们加点料

然后我们重新跑一下。

  • isConst: 变量声明是用的const关键字
  • isCallOf: 代码就不看了,简单的说一下就是判断这个node的赋值是否是一个CallExpression[12]节点。比如:const a = defineProps({});
  • isDefineCall: 用的const声明,然后等式右边用的defineProps[13]defineEmits[14]又或者是withDefaults[15]
  • Identifier[16]: 赋值的对象,比如const aaa = defineProps({}),这里的aaa就是indentifier,即变量的名字。
  • userReactiveBinding: 表达式右边是否用了reactive[17]
  • canNeverBeRef: 顾名思义,就是不可能变为ref的节点类型,有以下几种
  • + UnaryExpression[18]: 表达式语句中的一元表达式,比如delete xxx;就是一个一元表达式。
  • + BinaryExpression[19]: 表达式语句中的二元表达式,比如const a = b + c,这里的b + c就是一个二元表达式。
  • + ArrayExpression[20]: 这个不多说,大家都懂。
  • + ObjectExpression[21]: 这个也不多说。
  • + FunctionExpression[22]: 这个也不多说。
  • + ArrowFunctionExpression[23]: 这个依旧是不多说,箭头表达式。
  • + UpdateExpression[24]: 更新表达式,比如a++
  • + ClassExpression[25]:类表达式,上面提到过了。
  • + taggedTemplateExpression[26]: 标记模板文字表达式,很拗口。。找到个大佬的描述[27]。比如
  • + SequenceExpression[28]: 序列表达式, 比如: a = 1, b = 2, c = 3; 它自身需要递归处理,因为存在多个赋值,并不清楚里面的赋值里有没有ref赋值。
  • + 任何typeLiteral结尾的节点类型。
  • registerBinding: 顾名思义,就是将变量赋值的变量名和关键的几个api绑定到一起
  • ObjectPattern[29]: 如果用了var let const关键字但是却没有变量名,那么这个我们能猜到的就是es6的解构。比如:const { a } = b;
  • ArrayPattern[30]: 同上。
  • walkObjectPattern: 这个方法就不多说了,就是处理解构赋值的默认值这个问题。
  • walkArrayPattern: 同上。

ok,说了一大堆babel的东西,我们来总结下walkDeclaration这个方法。简单的说就是在分门别类。

梳理下逻辑(下面提到的refreactivedefinePropsdefineEmitswithDefaults均是vue3api,赋值指的是const a = ref(0)这样。)

  1. 判断你的类型,如果不是声明变量,那就判断是否是enum、function、class声明,如果是就直接绑定到bindings中,至于这个bindings是干啥的,简单的说就是准备setup导出的家伙。
  2. 如果是变量声明,判断是否是在声明解构(对象、数组解构),是则需要进一步判断解构的变量,然后将变量绑定到bindings中。
  3. 如果不是声明变量解构,那就是常规变量赋值表达式,有以下几个场景(场景从上到下if条件判断)
  • 用的是reactive赋值给变量并且是赋值给一个const变量,绑定并标记为setup-reactive-const类型;
  • 用的是reactive赋值给变量,但不是const变量,绑定并标记为setup-let类型;
  • 用的是defineProps赋值给变量并且是const变量,绑定并标记为setup-reactive-const;
  • 用的是defineEmits、withDefaults赋值给变量并且是const变量,绑定并标记为setup-const;
  • 用的是非ref``api(上面提到的那几个)并且是赋值给const变量,绑定并标记为setup-const;
  • 非以上场景,变量是const变量并且是用ref赋值给变量,绑定并标记为setup-ref
  • 非以上场景,变量是const变量绑定并标记为setup-maybe-ref;
  • 剩下的场景就都标记为setup-let;
  1. 这里还有一个特殊场景:props的解构,被skip掉了,就不多说了,毕竟reactivity被破坏掉了。

来看下数据


总结

今天主要是在分析非setup语法糖的script block。也知道了是如何处理import、export以及收集各种变量等东西,然后给他们分门别类。最后将非setup script block的迁移到setup语法糖的script block之前。

参考

  1. ^magic-string https://www.npmjs.com/package/magic-string
  2. ^@babel/parser https://babeljs.io/docs/en/babel-parser
  3. ^babel-importDeclaration https://babeljs.io/docs/en/babel-types#importdeclaration
  4. ^babel-importSpecifier https://babeljs.io/docs/en/babel-types#importspecifier
  5. ^babel-importDefaultSpecifier https://babeljs.io/docs/en/babel-types#importdefaultspecifier
  6. ^babel-ExportDefaultDeclaration https://babeljs.io/docs/en/babel-types#exportdefaultdeclaration
  7. ^babel-exportNamedDeclaration https://babeljs.io/docs/en/babel-types#exportnameddeclaration
  8. ^babel-variableDeclaration https://babeljs.io/docs/en/babel-types#variabledeclaration
  9. ^functionDeclaration https://babeljs.io/docs/en/babel-types#functiondeclaration
  10. ^babel-classDeclaration https://babeljs.io/docs/en/babel-types#classdeclaration
  11. ^@vue/reactivity-transform https://github.com/vuejs/core/tree/main/packages/reactivity-transform
  12. ^babel-callExpression https://babeljs.io/docs/en/babel-types#callexpression
  13. ^vue3-defineProps https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits
  14. ^vue3-defineEmit https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits
  15. ^vue3-withDefaults https://vuejs.org/api/sfc-script-setup.html#typescript-only-features
  16. ^babel-identifier https://babeljs.io/docs/en/babel-types#identifier
  17. ^vue3-reactive https://vuejs.org/api/reactivity-core.html#reactive
  18. ^babel-unaryExpression https://babeljs.io/docs/en/babel-types#unaryexpression
  19. ^babel-binaryExpression https://babeljs.io/docs/en/babel-types#binaryexpression
  20. ^babel-arrayExpression https://babeljs.io/docs/en/babel-types#arrayexpression
  21. ^babel-objectExpression https://babeljs.io/docs/en/babel-types#objectexpression
  22. ^babel-functionExpression https://babeljs.io/docs/en/babel-types#functionexpression
  23. ^babel-arrowFunctionExpression https://babeljs.io/docs/en/babel-types#arrowfunctionexpression
  24. ^babel-updateExpression https://babeljs.io/docs/en/babel-types#updateexpression
  25. ^babel-classExpression https://babeljs.io/docs/en/babel-types#classexpression
  26. ^babel-taggedTemplateExpression https://babeljs.io/docs/en/babel-types#taggedtemplateexpression
  27. ^taggedTemplateExpression http://www.javashuo.com/article/p-bmuslveb-h.html
  28. ^babel-sequenceExpression https://babeljs.io/docs/en/babel-types#sequenceexpression
  29. ^babel-objectPattern https://babeljs.io/docs/en/babel-types#objectpattern
  30. ^babel-arrayPattern https://babeljs.io/docs/en/babel-types#arraypattern

发布于 2022-11-24 09:18・IP 属地广东