上篇文章中我们分析确认了分析顺序,没看过的可以去看下。
坏蛋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
,不然就会有问题。
jsx
和typescript
兼容处理这一段很简单,就是对jsx
和ts
环境的判断和插件支持。
setup script
注:以下说的setup script block
都是说这种`` ,用了setup
语法糖的。
实际上我们的test suite
并不会执行这段代码,但是学习嘛,所以我又搞了个不带setup
的test suite
用来调试这段代码,我这里就不贴代码了,简单的vue2.x
代码即可。
这块代码量有点多,我们一点一点的分析。有些判断错误场景的就省略了,感兴趣的大佬可自行去看下。
什么情况下会执行这段代码?:不带有script setup
块并且这个script
块的lang
要么不存在(vue2.x
选项式写法或者3.x
非语法糖写法)要么就只能是ts、tsx、jsx
。
代码切割成一块一块来看下。
_parse
[1]: 这个是babel
的parse
,我之前的文章里有介绍,感兴趣的大佬可以看下。 是用来将代码转换成ast
的。
map
: 是source map
这里就不说了,因为不影响我们调试学习。
content
: script
的源码。
plugins
: 就是指的上面的那些个ts、jsx、tsx
以及如果你用了babel
的plugins
用来处理js
,那这里肯定也是得加上的,为了防止你的js
代码没有被babel
处理到。
这行代码一看就是用来处理绑定的属性的,来看下analyzeScriptBindings
的代码
analyzeScriptBindings
ExportDefaultDeclaration
[2]: 匹配到export default
关键词。
ObjectExpression
[3]: 对象表达式,export default
后面接的自然是{}
,也就是说export default
声明的对象是它后面的这个对象。
来看下analyzeBindingsFromOptions
的代码。整体是遍历这些node
(节点),然后做对应处理。 代码又是有些长,还得切块。
先来看下遍历中第一块代码
ObjectProperty
[4]: 顾名思义,就是一个对象属性Identifier
[5]: 这个属性的key
的类型是一个标识符,key
的 name
就是我们的inject、props、components
等这些。getObjectOrArrayExpressionKeys
: 这个就是用来处理对象格式或者是数组格式的值(比如props
支持两种写法props: ['a', 'b']
或者props: { a: String, b: String }
),代码咱就不看了,后面看下数据即可。methods
和computed
是必须接收一个对象,然后对象里都是方法才行。
接着看
ObjectMethod
[6]:比如这种data () { return {} }
。
其他就不多说了,我们直接来看下最终数据
总结下analyzeBindingsFromOptions
这个方法
简单的说就是在将你绑定的数据都转换成key-type
的形式。
回到之前的代码
开启reactivityTransform
这块代码我们先不看了,放到下面一个小标题单独分析,简单的说就是抽取reactivity-transform
里的api
再转换成runtime
想要的api
,比如$ref
变成import { ref as _ref } from 'vue'
,然后替换$ref
为ref
,然后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
变量如何注入有关,不过只用于常规非setup
的script``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
的代码处理。
简单的说就四件事
source
转换成ast
reactivity-transform
,如果开启了就判断是否存在相关api
,有就做替换处理,具体分析看上面css
变量,装换成setup
中的数据,然后存入runtime
代码中注入到setup
(这里的setup
并不是指setup script block
,而是当前script block
自带的,没有自带的则setup
就直接变成这段runtime
)中,等组件实例创建时执行。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
中设置的reactivityTransform
为true
这里就是true
,虽然这个变量还有两个可用于判断的options
字段,但是都是准备废弃的,我们就不多说了。MagicString
[10]: 一个第三方轻量包,可用来处理源码并生成source map
.s
: 自然就是被处理后的源码.startOffset
: type
为script
代码的起始位置。endOffset
: type
为script
代码的结束位置。transformAST
: 将代码转换成ast
,从@vue/reactivity-transform
中导入, 需要注意transformAST
方法里面其实还做了一步操作,把xxx = value
转换成xxx.value = value
。importedHelpers
: 这种一般都是辅助函数相关的,来看下数据。总结一下reactivity-transform
开启后做了什么
这一段其实很简单,就是判断是否开启了ractivity-transform
并且带有$ref
等这些用法,如果符合。先调用magic-string
处理source
转换为可操作的“源码”。然后调用@vue/reactivity-transform
的transformAST
找出目标api
,然后导入这个api
对应的原本的真实runtime
api
。比如$ref
变成import { ref as _ref } from 'vue'
,把xxx = value
转换成xxx.value = value
。导入真实的api
后再移除这个api
。如果需要source map
就生成source map
。
今天我们分析了compileScript
的第一部分(day1
)。
主要做了以下几件事
setup script block
的代码如果觉得对你有帮助麻烦点个赞,谢谢!
编辑于 2022-11-22 12:28・IP 属地广东