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

前言

上篇文章中我们分析确认了分析顺序,没看过的可以去看下。

坏蛋Dan:vue-loader源码分析学习

坏蛋Dan:vue/compiler-sfc源码分析学习1--如何调试

坏蛋Dan:vue/compiler-sfc源码分析学习--part1

坏蛋Dan:vue/compiler-sfc源码分析学习2--确认后续分析目标

请务必点个赞~

话不多说,我们直接看代码。


正文

在开始调试前说下,由于代码量有些大(1500+行),所以这里分开来一块一块的分析,并且新开一个part,根据日期拆分,比如今天是day1

入口

回到node_modules\vue-loader\dist\select.js中,我们看到type === script时先是调用resolveScript_1.resolveScript处理descriptor之后才执行loader的回调。

那么我们顺着这段代码进入node_modules\vue-loader\dist\resolveScript.js中找到resolveScript这个方法。

代码很简单,就是判断当前环境,然后将对应参数传入compiler_sfc_1.compileScript中也就是require(@vue/compiler-sfc).compileScript中。

切回我们的packages/compiler-sfc包中,找到index.ts,发现导出的compileScript是来自compileScript.ts的。


选择测试单元

在开始分析之前,我们需要确定测试单元,调试文件是compileScript.ts,那测试单元自然在compileScript.spec.ts文件中找。或者我们自己写一个,到时差了什么我们自己再加。

然后进入src/compileScript.ts中打上断点。

然后直接测试单元左上角debug

需要注意的一点是,如果你是自己手动调用的compileScript,请务必在调用之前先调用parse转换成descriptorSfcBlock,不然就会有问题。


jsxtypescript兼容处理

这一段很简单,就是对jsxts环境的判断和插件支持。


处理非setup script

注:以下说的setup script block都是说这种`` ,用了setup语法糖的。

实际上我们的test suite并不会执行这段代码,但是学习嘛,所以我又搞了个不带setuptest suite用来调试这段代码,我这里就不贴代码了,简单的vue2.x代码即可。

这块代码量有点多,我们一点一点的分析。有些判断错误场景的就省略了,感兴趣的大佬可自行去看下。

什么情况下会执行这段代码?:不带有script setup块并且这个script块的lang要么不存在(vue2.x选项式写法或者3.x非语法糖写法)要么就只能是ts、tsx、jsx

代码切割成一块一块来看下。

_parse[1]: 这个是babelparse,我之前的文章里有介绍,感兴趣的大佬可以看下。 是用来将代码转换成ast的。

map: 是source map这里就不说了,因为不影响我们调试学习。

content: script的源码。

plugins: 就是指的上面的那些个ts、jsx、tsx以及如果你用了babelplugins用来处理js,那这里肯定也是得加上的,为了防止你的js代码没有被babel处理到。

这行代码一看就是用来处理绑定的属性的,来看下analyzeScriptBindings的代码

analyzeScriptBindings

ExportDefaultDeclaration[2]: 匹配到export default关键词。

ObjectExpression[3]: 对象表达式,export default后面接的自然是{} ,也就是说export default声明的对象是它后面的这个对象。

来看下analyzeBindingsFromOptions的代码。整体是遍历这些node(节点),然后做对应处理。 代码又是有些长,还得切块。

先来看下遍历中第一块代码

  • ObjectProperty[4]: 顾名思义,就是一个对象属性
  • Identifier[5]: 这个属性的key的类型是一个标识符,keyname就是我们的inject、props、components等这些。
  • getObjectOrArrayExpressionKeys: 这个就是用来处理对象格式或者是数组格式的值(比如props支持两种写法props: ['a', 'b']或者props: { a: String, b: String }),代码咱就不看了,后面看下数据即可。

methodscomputed是必须接收一个对象,然后对象里都是方法才行。

接着看

ObjectMethod[6]:比如这种data () { return {} }

其他就不多说了,我们直接来看下最终数据

总结下analyzeBindingsFromOptions这个方法

简单的说就是在将你绑定的数据都转换成key-type的形式。

回到之前的代码

开启reactivityTransform

这块代码我们先不看了,放到下面一个小标题单独分析,简单的说就是抽取reactivity-transform里的api再转换成runtime想要的api,比如$ref变成import { ref as _ref } from 'vue',然后替换$refref,然后xxx = value替换为xxx.value = value

处理cssVars

由于我们这个测试单元并没有cssVar,所以我们来加一下

重新跑下

我们继续分析

  • DEFAULT_VAR: __default__
  • plugins: 前面提到过的,比如typescript、jsx、tsx或者babel带上的用于处理js等的插件名字。
  • content: 被处理后的源码。
  • rewriteDefault: 看名字猜测是用来处理default关键词的。代码有些长,就不看代码了,简单说一下,就是判断是否有export default关键词,然后转换为const __default__ = xxx,比如export default { a: 123 }会被转换为const __default__ = { a: 123 }
  • genNormalScriptCssVarsCode: 这个还是比较重要的,和css变量如何注入有关,不过只用于常规非setupscript``block,看下代码。

这里说一下,一般一段可执行代码是字符串,那这段代码大概率就是一段runtime,需要在编译后执行的。

  • CSS_VARS_HELPER: useCssVars,是runtime辅助函数。 代码位置在@vue/runtime-dom/helper/cssVars.ts中,来看下代码。

里面有挺多东西我们目前都不清楚是干啥的,但是这并不影响我们分析。大概意思就是创建VNode的时候将这些变量插入到vNode中,然后监听变化。

  • genCssVarsCode: 来看下代码,看下是如何处理的
  • genCssVarsFromList: 将css var变成js认识的东西。
  • id: 自然就是scopedId

来看下数据,由于截图不好截,所以这里打印一下。

可以看到变成了一个对象格式的数据。

我们接着分析

  • createSimpleExpression: 从@vue/compiler-dom[7]中导入,前面也有提到过,这个包是用来将代码转换成ast的。代码就不看了,直接看下数据即可。

  • createRoot: 同上,创建编译对象。
  • createTransformContext: 同上,编译的上下文环境, 直接看下数据。

  • processExpression: 同上,装换成能被js认识的ast (?)

最终变成

最后返回runtime

_useCssVars(_ctx => (${transformedString}))

总结下genCssVarsCode

这个方法调用@vue/compiler-dom的方法,将css var变量转换为['${scopedId}-${var}']: context[var],其中context也就是组件实例上下文。最终返回一段runtime _useCssVars(_ctx => (${transformedString}))

总结一下genNormalScriptCssVarsCode

这段runtime代码很简单,就是将cssVars转换成代码,然后注入到setup里面,毕竟是vue3.x的写法,所以只能是setup里才有效。 然后return这段runtime。 最终这段runtime会在组件实例创建的时候执行。

回到我们的处理cssVars这段代码

最后把js导出。

回到我们的处理非setup script代码中,最后将数据return出去。

总结一下这段非setup script block的代码处理。

简单的说就四件事

  1. source转换成ast
  2. 判断是否开启reactivity-transform,如果开启了就判断是否存在相关api,有就做替换处理,具体分析看上面
  3. 处理css变量,装换成setup中的数据,然后存入runtime代码中注入到setup(这里的setup并不是指setup script block,而是当前script block自带的,没有自带的则setup就直接变成这段runtime)中,等组件实例创建时执行。
  4. 最后返回。

reactivityTransform

这个是vue3.x的新特性,目前还在实验中,可以理解为是一种语法糖。

在看例子前,我们先回想下我们之前在写vue3代码时,是否会经常忘记ref处理后的数据赋值时需要用.value,然后直接赋值导致数据不正常改变。

我们来看下例子

例子:

$ref: 这个其实就是ref这个api,但你应该发现了count赋值并不需要count.value++。是的,这样就可以了。可以不用再别扭的写count.value这种语法。

这块代码会在compiler也就是编译的时候被转换成ref一样的代码。

类似的api还有

简单的说,就是提供一些新的api,这些api可以免去笨重的写法,它们会在编译过程中转换为相应的写法。

另外这些个api都是全局导入的(前提是开启了reactivity transform),当然你也可以从vue/macros中导入,这样会直观、好追踪些。

我们来改动下我们的代码,来看下这里面是怎么处理的。

然后compile方法第二个参数options新增reactivityTransform: true

重新debug一下我们的demo,然后回到上面那个小标题的代码中。

  • @vue/reactivity-transform[9]: 这个就是处理这个新特性的核心包。后面有空会分析这个包。
  • shouldTransform: 代码就不看了,就一个正则匹配$ref等。
  • enableReactivityTransform:我们的options中设置的reactivityTransformtrue这里就是true,虽然这个变量还有两个可用于判断的options字段,但是都是准备废弃的,我们就不多说了。
  • MagicString[10]: 一个第三方轻量包,可用来处理源码并生成source map.
  • s: 自然就是被处理后的源码.
  • startOffset: typescript代码的起始位置。
  • endOffset: typescript代码的结束位置。
  • transformAST: 将代码转换成ast,从@vue/reactivity-transform中导入, 需要注意transformAST方法里面其实还做了一步操作,把xxx = value转换成xxx.value = value
  • importedHelpers: 这种一般都是辅助函数相关的,来看下数据。

总结一下reactivity-transform开启后做了什么

这一段其实很简单,就是判断是否开启了ractivity-transform并且带有$ref等这些用法,如果符合。先调用magic-string处理source转换为可操作的“源码”。然后调用@vue/reactivity-transformtransformAST找出目标api,然后导入这个api对应的原本的真实runtime api。比如$ref变成import { ref as _ref } from 'vue',把xxx = value转换成xxx.value = value。导入真实的api后再移除这个api。如果需要source map就生成source map


总结

今天我们分析了compileScript的第一部分(day1)。

主要做了以下几件事

  1. 找入口,找测试单元
  2. 初始环境分析
  3. 分析非setup script block的代码

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

参考

  1. ^@babel/parser https://babeljs.io/docs/en/babel-parser
  2. ^babel-ExportDefaultDeclaration https://babeljs.io/docs/en/babel-types#exportdefaultdeclaration
  3. ^babel-ObjectExpression https://babeljs.io/docs/en/babel-types#objectexpression
  4. ^babel-objectProperty https://babeljs.io/docs/en/babel-types#objectproperty
  5. ^babel-identifier https://babeljs.io/docs/en/babel-types#identifier
  6. ^ObjectMethod https://babeljs.io/docs/en/babel-types#objectmethod
  7. ^@vue/compiler-dom https://github.com/vuejs/core/tree/main/packages/compiler-dom
  8. ^vue-reactivityTransform https://vuejs.org/guide/extras/reactivity-transform.html
  9. ^@vue/reactivity-transform https://github.com/vuejs/core/tree/main/packages/reactivity-transform
  10. ^magic-string https://www.npmjs.com/package/magic-string

编辑于 2022-11-22 12:28・IP 属地广东