Example Analysis of AST Parser in Vue3
小编给大家分享一下Vue3中AST解析器的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!
1、生成 AST 抽象语法树
首先我们来重温一下 baseCompile 函数中有关 ast 的逻辑及后续的使用:
export function baseCompile( template: string | RootNode, options: CompilerOptions = {}): CodegenResult { /* 忽略之前逻辑 */ const ast = isString(template) ? baseParse(template, options) : template transform( ast, {/* 忽略参数 */} ) return generate( ast, extend({}, options, { prefixIdentifiers }) )}
因为我已经将咱们不需要关注的逻辑注释处理,所以现在看函数体内的逻辑会非常清晰:
生成 ast 对象
将 ast 对象作为参数传入 transform 函数,对 ast 节点进行转换操作
将 ast 对象作为参数传入 generate 函数,返回编译结果
这里我们主要关注 ast 的生成。可以看到 ast 的生成有一个三目运算符的判断,如果传进来的 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.HTML const nodes: TemplateChildNode[] = [] // 存储解析后的节点 // 当标签未闭合时,解析对应节点 while (!isEnd(context, mode, ancestors)) {/* 忽略逻辑 */} // 处理空白字符,提高输出效率 let removedWhitespace = false if (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 属性即可。
接下来会判断,如果第一个字符是 "