Vue3|Vue3 AST解析器-源码解析
目录
- 1、生成 AST 抽象语法树
- 2、创建 AST 的根节点
- 3、解析子节点
- 4、解析模板元素 Element
- 5、示例:模板元素解析
packges/vue/src/index.ts
的入口开始,了解了一个 Vue 对象的编译流程,在文中我们提到 baseCompile
函数在执行过程中会生成 AST 抽象语法树,毫无疑问这是很关键的一步,因为只有拿到生成的 AST 我们才能遍历 AST 的节点进行 transform 转换操作,比如解析 v-if
、v-for
等各种指令,或者对节点进行分析将满足条件的节点静态提升,这些都依赖之前生成的 AST 抽象语法树。那么今天我们就一起来看一下 AST 的解析,看看 Vue 是如何解析模板的。1、生成 AST 抽象语法树 首先我们来重温一下
baseCompile
函数中有关 ast 的逻辑及后续的使用:export function baseCompile(template: string | RootNode,options: CompilerOptions = {}): CodegenResult {/* 忽略之前逻辑 */const ast = isString(template) ? baseParse(template, options) : templatetransform(ast,{/* 忽略参数 */})return generate(ast,extend({}, options, {prefixIdentifiers}))}
因为我已经将咱们不需要关注的逻辑注释处理,所以现在看函数体内的逻辑会非常清晰:
- 生成 ast 对象
- 将
ast
对象作为参数传入transform
函数,对ast
节点进行转换操作 - 将 ast 对象作为参数传入
generate
函数,返回编译结果
template
模板参数是一个字符串,那么则调用 baseParse
解析模板字符串,否则直接将 template
作为 ast
对象。baseParse
里做了什么事情才能生成 ast 呢?一起来看一下源码,export function baseParse(content: string,options: ParserOptions = {}): RootNode {const context = createParserContext(content, options) // 创建解析的上下文对象const start = getCursor(context) // 生成记录解析过程的游标信息return createRoot( // 生成并返回 root 根节点parseChildren(context, TextModes.DATA, []), // 解析子节点,作为 root 根节点的 children 属性getSelection(context, start))}
在
baseParse
的函数中我添加了注释,方便大家理解各个函数的作用,首先会创建解析的上下文,之后根据上下文获取游标信息,由于还未进行解析,所以游标中的 column
、line
、offset
属性对应的都是 template
的起始位置。之后就是创建根节点并返回根节点,至此ast 树生成,解析完成。2、创建 AST 的根节点
export function createRoot(children: TemplateChildNode[],loc = locStub): RootNode {return {type: NodeTypes.ROOT,children,helpers: [],components: [],directives: [],hoists: [],imports: [],cached: 0,temps: 0,codegenNode: undefined,loc}}
看
createRoot
函数的代码,我们能发现该函数就是返回了一个 RootNode
类型的根节点对象,其中我们传入的 children 参数会被作为根节点的 children
参数。这里非常好理解,按树型数据结构来想象就可以。所以生成 ast 的关键点就会聚焦到 parseChildren
这个函数上来。parseChildren
函数如果不去看它的源码,见文之意也可以大致了解这是一个解析子节点的函数。接下来我们就来一起来看一下 AST 解析中最关键的 parseChildren
函数,还是老规矩,为了帮助大家理解,我会精简函数体内的逻辑。3、解析子节点
function parseChildren(context: ParserContext,mode: TextModes,ancestors: ElementNode[]): TemplateChildNode[] {const parent = last(ancestors) // 获取当前节点的父节点const ns = parent ? parent.ns : Namespaces.HTMLconst nodes: TemplateChildNode[] = [] // 存储解析后的节点// 当标签未闭合时,解析对应节点while (!isEnd(context, mode, ancestors)) {/* 忽略逻辑 */}// 处理空白字符,提高输出效率let removedWhitespace = falseif (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {/* 忽略逻辑 */}// 移除空白字符,返回解析后的节点数组return removedWhitespace ? nodes.filter(Boolean) : nodes}
从上文代码中,可以知道
parseChildren
函数接收三个参数,context
:解析器上下文,mode
:文本数据类型,ancestors
:祖先节点数组。而函数的执行中会首先从祖先节点中获取当前节点的父节点,确定命名空间,以及创建一个空数组,用来储存解析后的节点。之后会有一个 while 循环,判断是否到达了标签的关闭位置,如果不是需要关闭的标签,则在循环体内对源模板字符串进行分类解析。之后会有一段处理空白字符的逻辑,处理完成后返回解析好的 nodes 数组。在大家对于 parseChildren
的执行流程有一个初步理解之后,我们一起来看一下函数的核心,while 循环内的逻辑。在 while 中解析器会判断文本数据的类型,只有当
TextModes
为 DATA 或 RCDATA 时会继续往下解析。第一种情况就是判断是否需要解析 Vue 模板语法中的 “
Mustache
”语法 (双大括号) ,如果当前上下文中没有 v-pre 指令来跳过表达式,并且源模板字符串是以我们指定的分隔符开头的(此时 context.options.delimiters
中是双大括号),就会进行双大括号的解析。这里就可以发现,如果当你有特殊需求,不希望使用双大括号作为表达式插值,那么你只需要在编译前改变选项中的 delimiters
属性即可。接下来会判断,如果第一个字符是 “<” 并且第二个字符是 '!'的话,会尝试解析注释标签
,和 这三种情况,对于 DOCTYPE 会进行忽略,解析成注释。
之后会判断当第二个字符是 “/” 的情况,“” 已经满足了一个闭合标签的条件了,所以会尝试去匹配闭合标签。当第三个字符是 “>”,缺少了标签名字,会报错,并让解析器的进度前进三个字符,跳过 “>”。
如果“”开头,并且第三个字符是小写英文字符,解析器会解析结束标签。
如果源模板字符串的第一个字符是 “<”,第二个字符是小写英文字符开头,会调用 parseElement
函数来解析对应的标签。
当这个判断字符串字符的分支条件结束,并且没有解析出任何 node 节点,那么会将 node 作为文本类型,调用 parseText 进行解析。
最后将生成的节点添加进 nodes
数组,在函数结束时返回。
【Vue3|Vue3 AST解析器-源码解析】这就是 while 循环体内的逻辑,且是 parseChildren
中最重要的部分。在这个判断过程中,我们看到了双大括号语法的解析,看到了注释节点的怎样被解析的,也看到了开始标签和闭合标签的解析,以及文本内容的解析。精简后的代码在下方框中,大家可以对照上述的讲解,来理解一下源码。当然,源码中的注释也是非常详细了哟。
while (!isEnd(context, mode, ancestors)) {const s = context.sourcelet node: TemplateChildNode | TemplateChildNode[] | undefined = undefinedif (mode === TextModes.DATA || mode === TextModes.RCDATA) {if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {/* 如果标签没有 v-pre 指令,源模板字符串以双大括号 `{{` 开头,按双大括号语法解析 */node = parseInterpolation(context, mode)} else if (mode === TextModes.DATA && s[0] === '<') {// 如果源模板字符串的第以个字符位置是 `!`if (s[1] === '!') {// 如果以 '
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- CGI,FastCGI,PHP-CGI与PHP-FPM
- Quartz|Quartz 源码解析(四) —— QuartzScheduler和Listener事件监听
- Java内存泄漏分析系列之二(jstack生成的Thread|Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析)
- [源码解析]|[源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3)
- Android系统启动之init.rc文件解析过程
- 小程序有哪些低成本获客手段——案例解析
- Spring源码解析_属性赋值
- Android下的IO库-Okio源码解析(一)|Android下的IO库-Okio源码解析(一) 入门
- 08_JVM学习笔记_类命名空间解析
- WebSocket|WebSocket 语法解析