[关闭]
@wy 2020-12-09T10:31:18.000000Z 字数 22432 阅读 748

Vue3源码-- 解析器 parse

vue3源码


parse 用来将模板字符串,解析成 AST (Abstract Syntax Tree, 抽象语法树) 结构,该结构是用对象来描述与之对应的模板中的每个节点信息,后续进行转换、优化、生成渲染函数时,便基于该对象信息进行。

安装 @vue/compiler-core 模块, 创建 app.js, 运行查看。

  1. // app.js
  2. const {baseParse} = require('@vue/compiler-core');
  3. const template = `
  4. <div>hello world</div>
  5. <div>
  6. <span>{{hello}}</span>
  7. </div>
  8. `
  9. const ast = baseParse(template);
  10. console.log(JSON.stringify(ast, null, 2));

在命令行中输入 node app.js > ast.json,运行后,会创建 ast.json 文件,打开 ast.json 查看结构如下:

  1. {
  2. "type": 0,
  3. "children": [
  4. {
  5. "type": 1,
  6. "ns": 0,
  7. "tag": "div",
  8. "tagType": 0,
  9. "props": [],
  10. "isSelfClosing": false,
  11. "children": [
  12. {
  13. "type": 2,
  14. "content": "hello world",
  15. "loc": {
  16. "start": {
  17. "column": 6,
  18. "line": 2,
  19. "offset": 6
  20. },
  21. "end": {
  22. "column": 17,
  23. "line": 2,
  24. "offset": 17
  25. },
  26. "source": "hello world"
  27. }
  28. }
  29. ],
  30. "loc": {
  31. "start": {
  32. "column": 1,
  33. "line": 2,
  34. "offset": 1
  35. },
  36. "end": {
  37. "column": 23,
  38. "line": 2,
  39. "offset": 23
  40. },
  41. "source": "<div>hello world</div>"
  42. }
  43. },
  44. {
  45. "type": 1,
  46. "ns": 0,
  47. "tag": "div",
  48. "tagType": 0,
  49. "props": [],
  50. "isSelfClosing": false,
  51. "children": [
  52. {
  53. "type": 1,
  54. "ns": 0,
  55. "tag": "span",
  56. "tagType": 0,
  57. "props": [],
  58. "isSelfClosing": false,
  59. "children": [
  60. {
  61. "type": 5,
  62. "content": {
  63. "type": 4,
  64. "isStatic": false,
  65. "isConstant": false,
  66. "content": "hello",
  67. "loc": {
  68. "start": {
  69. "column": 11,
  70. "line": 4,
  71. "offset": 40
  72. },
  73. "end": {
  74. "column": 16,
  75. "line": 4,
  76. "offset": 45
  77. },
  78. "source": "hello"
  79. }
  80. },
  81. "loc": {
  82. "start": {
  83. "column": 9,
  84. "line": 4,
  85. "offset": 38
  86. },
  87. "end": {
  88. "column": 18,
  89. "line": 4,
  90. "offset": 47
  91. },
  92. "source": "{{hello}}"
  93. }
  94. }
  95. ],
  96. "loc": {
  97. "start": {
  98. "column": 3,
  99. "line": 4,
  100. "offset": 32
  101. },
  102. "end": {
  103. "column": 25,
  104. "line": 4,
  105. "offset": 54
  106. },
  107. "source": "<span>{{hello}}</span>"
  108. }
  109. }
  110. ],
  111. "loc": {
  112. "start": {
  113. "column": 1,
  114. "line": 3,
  115. "offset": 24
  116. },
  117. "end": {
  118. "column": 7,
  119. "line": 5,
  120. "offset": 61
  121. },
  122. "source": "<div>\n <span>{{hello}}</span>\n</div>"
  123. }
  124. }
  125. ],
  126. "helpers": [],
  127. "components": [],
  128. "directives": [],
  129. "hoists": [],
  130. "imports": [],
  131. "cached": 0,
  132. "temps": 0,
  133. "loc": {
  134. "start": {
  135. "column": 1,
  136. "line": 1,
  137. "offset": 0
  138. },
  139. "end": {
  140. "column": 1,
  141. "line": 6,
  142. "offset": 62
  143. },
  144. "source": "\n<div>hello world</div>\n<div>\n <span>{{hello}}</span>\n</div>\n"
  145. }
  146. }

每个对象用来描述一个节点信息,其中 type 为节点类型,用数字表示;tag 是标签的名称;props 为行间属性的集合;children 为嵌套的子级节点对象集合;loc 为解析到该字符串在整个源模板中的位置信息。还包含其他的属性就不一一列举说明了,之后分析到每个解析方法会有涉及。

可以看出来 AST 对象也是个嵌套结构,与模板的嵌套关系一致。唯一不同的是最顶层是一个虚拟节点 type0,作为整个 AST 的根对象, 不映射模板中任何一个具体的节点。

为什么要把根节点设计成一个虚拟节点呢?

因为在 Vue3 中支持 Fragment 语法,也就是一个组件可以有多个根节点,而不是像 Vue2 那样只能有一个根节点。

  1. <template>
  2. <div>hello</div>
  3. <div>world</div>
  4. </template>

这在 Vue2 中会报错,提示模板只能有一个 root 节点。而在 Vue3 中是被允许的,正是在最外层设计的虚拟根节点发挥的作用。

这个根节点上还有其他的属性,像 helpershoistsimports 等属性会在后续的对 AST 转换时进行赋值,到生成渲染函数时会用到。

解析器 parseVue 中属于核心功能,那它是如何将模板解析为 AST 的呢?本文将逐行揭示解析器内部的运行原理。

编译器调用入口

@vue/compiler-core 目录文件的 compile.ts 文件,找到编译器入口:

  1. export function baseCompile(
  2. template: string | RootNode,
  3. options: CompilerOptions = {}
  4. ): CodegenResult {
  5. // 其他省略
  6. // 模板解析器 => ast 对象
  7. const ast = isString(template) ? baseParse(template, options) : template
  8. // 转换、优化
  9. transform(ast, {})
  10. // 生成渲染函数
  11. return generate(ast,{})
  12. }

整个编译器主要做了三件事,
- 模板字符串解析为 AST 对象
- 对 AST 进行转换、优化
- 生成渲染函数

本章主要解析解析器的实现。

baseCompile 接收两个参数,从 ts 定义的类型中能够看到,template 为字符串,或者是一个已经解析后的 AST 对象。我们把目标主要集中在解析模板字符串上。

第二个参数 options 的类型是一个 CompilerOptions,编译辅助选项,,。

找到定义它的位置,在 compiler-core/src/options.ts 文件中:

  1. export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions

定义了多种类型,从名字上可以看出来是给不同阶段使用的参数,我们具体来看 ParserOptions 类型:

当在不同平台调用 baseCompile 函数时,可以根据需要传入以上的解析辅助选项,会在解析模板阶段使用到,不传也没有关系,内部会定义默认的选项。

解析函数 baseParse

找到文件 parse.ts,定位到 baseParse 函数,整个解析器的入口。

  1. export function baseParse(
  2. content: string,
  3. options: ParserOptions = {}
  4. ): RootNode {
  5. const context = createParserContext(content, options)
  6. const start = getCursor(context)
  7. return createRoot(
  8. parseChildren(context, TextModes.DATA, []),
  9. getSelection(context, start)
  10. )
  11. }

此函数主要做三件事,创建解析上下文 contextparseChildren 解析模板字符串创建根节点 root

参数说明,content 待解析的模板字符串;options 类型是 ParserOptions,为解析配置项。

返回值是解析后的 AST 对象。

ParserOptions 解析配置项

baseParse 函数解析模板字符串与平台无关,只要调用最终都能生成 AST 对象,只是在不同的平台对标签的规定有差异,需要通过特定的配置项来实现。配置项会在解析阶段被调用,能够影响最终的解析结果。例如 input 这个标签,在浏览器平台属于自闭合标签,通过配置项告知解析器,这样在解析到这类标签时,便不会继续解析子级了。

首先看下有哪些配置项,找到定义它的位置,在 compiler-core/src/options.ts 文件中:

  1. export interface ParserOptions {
  2. /**
  3. * 是否是平台原生的元素,例如在浏览器中有 div、span、input等。
  4. */
  5. isNativeTag?: (tag: string) => boolean
  6. /**
  7. * 是否是自闭合标签。例如:<img />, <input />, <hr /> 等
  8. */
  9. isVoidTag?: (tag: string) => boolean
  10. /**
  11. * 是否是pre标签,作用是保留标签内的空格和换行符
  12. */
  13. isPreTag?: (tag: string) => boolean
  14. /**
  15. * 是否是跟平台相关指定的内置组件. 例如:Vue内置组件`<Transition>`、<TransitionGroup>
  16. */
  17. isBuiltInComponent?: (tag: string) => symbol | void
  18. /**
  19. * 是否是扩展的自定义标签,避免当成组件来解析。
  20. */
  21. isCustomElement?: (tag: string) => boolean | void
  22. /**
  23. * 获取到元素的命名空间,例如针对浏览器,在compiler-dom中定义了 HTML、SVG、MATH_ML
  24. */
  25. getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
  26. /**
  27. * 获取文本的解析模式
  28. */
  29. getTextMode?: (
  30. node: ElementNode,
  31. parent: ElementNode | undefined
  32. ) => TextModes
  33. /**
  34. * 自定义插值分隔符,默认为 ['{{', '}}']
  35. */
  36. delimiters?: [string, string]
  37. /**
  38. * 仅DOM编译器需要,解析实体字符,例如:$gt 解析为 > $lt 解析为 <
  39. */
  40. decodeEntities?: (rawText: string, asAttr: boolean) => string
  41. onError?: (error: CompilerError) => void
  42. /**
  43. * 是否将注释保留在AST树中,生产环境也保留
  44. */
  45. comments?: boolean
  46. }

从语义化属性名上很清晰的表达了配置项的作用,可以结合解析浏览器平台的 DOM 标签来理解。

找到 @vue/compiler-dom 模块的 parserOptions.ts 文件,可以看出定义的配置项,都是和浏览器平台相关。拿 isNativeTag 这个来说,用来判断是不是原生标签。

  1. isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),

原生标签有 html 标签和 SVG 标签,通过 isHTMLTag 继续找,能看到定义:

  1. const HTML_TAGS =
  2. 'html,body,base,head,link,meta,style,title,address,article,aside,footer,' +
  3. 'header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,' +
  4. 'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,' +
  5. 'data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,' +
  6. 'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,' +
  7. 'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,' +
  8. 'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,' +
  9. 'option,output,progress,select,textarea,details,dialog,menu,' +
  10. 'summary,template,blockquote,iframe,tfoot'

这里列出了 html 规定的标签,只要在解析模板字符串的过程中,标签名称不属于上述的原生标签,可以判定为是组件标签了。

以上只举了一个例子来说明配置项的作用,如果你要基于该解析器指定自己需要的平台的标签,可通过配置项来完成。

创建解析上下文对象

createParserContext 用来创建解析上下文对象,作用是记录解析过程中的解析状态,这个对象将贯穿解析的整个过程,会不断传递给不同的解析函数使用,来看下 createParserContext 内部做了哪些事:

  1. function createParserContext(
  2. content: string,
  3. rawOptions: ParserOptions
  4. ): ParserContext {
  5. // 克隆一份默认的编译辅助选项
  6. const options = extend({}, defaultParserOptions)
  7. // 将默认编译辅助选项覆盖为传入的选项
  8. for (const key in rawOptions) {
  9. // @ts-ignore
  10. options[key] = rawOptions[key] || defaultParserOptions[key]
  11. }
  12. return {
  13. options, // 编译辅助选项
  14. column: 1, // 列
  15. line: 1, // 行
  16. offset: 0, // 偏移位置
  17. originalSource: content, // 保留原始字符串
  18. source: content, // 记录字符串,之后会不断的截取
  19. inPre: false, // 是否是pre标签,或在pre标签内
  20. inVPre: false // 是否在 v-pre 指令所在标签,或在其内
  21. }
  22. }

此函数主要是将 传入的解析配置项覆盖默认的解析配置项返回初始一些列值的对象

默认解析配置项

defaultParserOptions 定义了默认的解析配置项,会被传入的配置项覆盖。看一下 defaultParserOptions的定义:

  1. const decodeRE = /&(gt|lt|amp|apos|quot);/g
  2. const decodeMap: Record<string, string> = {
  3. gt: '>',
  4. lt: '<',
  5. amp: '&',
  6. apos: "'",
  7. quot: '"'
  8. }
  9. export const defaultParserOptions: MergedParserOptions = {
  10. delimiters: [`{{`, `}}`], // 默认的插值符号
  11. getNamespace: () => Namespaces.HTML, // 默认命名空间,为 0
  12. getTextMode: () => TextModes.DATA, // 默认的文本模式, 为0
  13. isVoidTag: NO, // 自闭标签判断函数
  14. isPreTag: NO, // pre 标签判断函数
  15. isCustomElement: NO, // 自定义标签判断函数
  16. // 实体字符转对应的符号
  17. decodeEntities: (rawText: string): string =>
  18. rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
  19. onError: defaultOnError, // 错误提示函数
  20. comments: false // 是否保留注释节点,默认不保留
  21. }

NO 是一个函数,作用就是返回 false,这种方式可以借鉴,当多个变量需要定义默认的函数时,可以只定义一个函数,多次赋值,节省创建函数的成本,减少内存占有。

  1. export const NO = () => false

decodeEntities 的作用就是匹配到以 & 开头的实体字符,替换为对应的字符标识。

getCursor 获取位置

回到 baseParse,来看 getCursor 函数,作用很简单,拿到调用时解析到的列、行、偏移信息。

  1. function getCursor(context: ParserContext): Position {
  2. const { column, line, offset } = context
  3. return { column, line, offset }
  4. }

此时调用,其实获取的就是第1列、第1行、第0个偏移,也就是模板字符串的开头位置。随着解析的进行,这些值会不断发生变化。

createRoot 创建根节点

createRoot,用来创建根对象,代码在 ast.ts 中,只需要记住返回的对象类型 typeNodeTypes.ROOT 。子级是一个数组,存的是传入的模板字符串解析后的 AST 对象。

parseChildren 解析子级

调用 parseChildren,才开始真正的解析模板字符串。

  1. parseChildren(context, TextModes.DATA, [])

context 解析上下文对象,其中的 source 存的就是要解析的模板字符串。

TextModes.DATA,是文本模式。

TextModes 文本模式

找到 TextModes 的定义:

  1. export const enum TextModes {
  2. // | Elements | Entities | End sign | Inside of
  3. DATA, // | ✔ | ✔ | End tags of ancestors |
  4. RCDATA, // | ✘ | ✔ | End tag of the parent | <textarea>
  5. RAWTEXT, // | ✘ | ✘ | End tag of the parent | <style>,<script>
  6. CDATA,
  7. ATTRIBUTE_VALUE
  8. }

RAWTEXT 原始文本元素,标签内可以容纳为文本形式,例如 \\,可以写写 css 样式和 js 代码。

嵌套栈

第三个参数,是一个空数组,当做栈来用,后进先出,作用是保存开始解析,却还没完成的标签,当解析时,遇到开始标签时,将标签推入栈中,遇到结束标签时,才将标签从栈中弹出。还有个作用在解析某个标签时,通过 **stack[stack.length - 1] **获取它的父元素。举个例子,有以下嵌套模板:

  1. <div>
  2. <span>hello</span>
  3. <ul>
  4. <li>1</li>
  5. <li>2</li>
  6. </ul>
  7. </section>

结束标签故意写错,解析过程为:

解析到 div 开始标签,标签名入栈:
['div']
继续解析,到 span 开始标签,标签名入栈:
['div','span']
继续解析,到 span 结束标签,解析完毕,则出栈:
['div']
继续解析,到 ul 开始标签,标签名入栈:
['div', 'ul']
继续解析,第一个 li 开始标签,标签名入栈:
['div', 'ul', 'li']
继续解析,第一个 li 结束标签,解析完毕,则出栈:
['div', 'ul']
继续解析,第二个 li 开始标签,标签名入栈:
['div', 'ul', 'li']
继续解析,第二个 li 结束标签,解析完毕,则出栈:
['div', 'ul']
继续解析,到 ul 结束标签,解析完毕,则出栈:
['div']
继续解析,到 section 结束标签,解析完毕,此时和栈内最后一个标签 'div', 不相同,则报错提醒。

通过不断将开始标签入栈,结束出栈,来判断标签是否解析完毕,并能在解析结束之后,找到该元素的父级,即为数组中最后一个元素。例如上面过程,当每个 li 解析完毕时,数组存的最后一个 ul,就是当前 li 的父级;ul 解析完毕时,数组之后一个 div,就是 ul 的父级。

举例说明入栈为标签,在实际的解析过程,入栈的是解析当前标签的对象形式,我们这里为了说明这个过程,简化成了入栈为标签,为了更方便理解。

parseChildren 内部实现

通过名字 parseChildren,能看出来,用来解析子级。每当解析到一个标签,进入到子级就会调用此函数。在内部会通过传入的模板字符串,命中解析规则,来决定调用那个解析函数进行解析。解析函数内部如果需要解析子级,又会调用 parseChildren 函数,这样会形成递归调用。

代码量比较庞大,分段来说明。

1. 整体结构

  1. function parseChildren(
  2. context: ParserContext,
  3. mode: TextModes,
  4. ancestors: ElementNode[]
  5. ): TemplateChildNode[] {
  6. const parent = last(ancestors)
  7. const ns = parent ? parent.ns : Namespaces.HTML
  8. const nodes: TemplateChildNode[] = []
  9. while (!isEnd(context, mode, ancestors)) {
  10. // 判断解析的字符传不能为空
  11. __TEST__ && assert(context.source.length > 0)
  12. const s = context.source
  13. let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
  14. // 解析规则
  15. // 省略....
  16. if(){}
  17. // node为空,没有命中上面的规则,则为文本形式,进入文本解析流程
  18. if (!node) {
  19. node = parseText(context, mode)
  20. }
  21. // 解析可能是兄弟节点,为数组形式,则分别放入到结果集中
  22. if (isArray(node)) {
  23. for (let i = 0; i < node.length; i++) {
  24. pushNode(nodes, node[i])
  25. }
  26. } else {
  27. pushNode(nodes, node)
  28. }
  29. }
  30. let removedWhitespace = false
  31. // 去掉空白字符
  32. return removedWhitespace ? nodes.filter(Boolean) : nodes
  33. }

该函数主要做的事情:
- 不断循环模板字符串,直到该标签解析完毕
- 命中不同规则,调用对应解析函数
- 将解析后的对象返回

此函数接收的三个参数,在上面解释过了,可以回看。特别要说明的是 ancestors,实际上就是上面说的存储嵌套关系的栈,存的是 ElementNode 类型,也就是解析后的对象。

1.1 找父级

找到当前开始解析的子级的父对象,也就是栈内最后一个值。

  1. const parent = last(ancestors)

last 函数的实现代码:

  1. function last<T>(xs: T[]): T | undefined {
  2. return xs[xs.length - 1]
  3. }

一开始没有父级,返回为 undefined

1.2 while循环

先看循环的大框架。

  1. while (!isEnd(context, mode, ancestors)) {
  2. const s = context.source
  3. let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
  4. // 判断类型,进入不同解析流程,下面会解释
  5. // 规则省略....
  6. if(){}
  7. }

循环的作用是不断解析模板字符串,直到将一个标签解析完毕为止。

通过 context.source 存的字符串的第一个字符,来命中指定的规则,走不同分支调用函数解析。规则比较多,稍后解释。

isEnd 判断标签解析完毕

先来看 isEnd 做的事,在什么样的情况下,该标签算是解析完毕。

  1. function isEnd(
  2. context: ParserContext,
  3. mode: TextModes,
  4. ancestors: ElementNode[]
  5. ): boolean {
  6. const s = context.source
  7. switch (mode) {
  8. case TextModes.DATA:
  9. if (startsWith(s, '</')) {
  10. //TODO: probably bad performance
  11. for (let i = ancestors.length - 1; i >= 0; --i) {
  12. if (startsWithEndTagOpen(s, ancestors[i].tag)) {
  13. return true
  14. }
  15. }
  16. }
  17. break
  18. case TextModes.RCDATA:
  19. case TextModes.RAWTEXT: {
  20. const parent = last(ancestors)
  21. if (parent && startsWithEndTagOpen(s, parent.tag)) {
  22. return true
  23. }
  24. break
  25. }
  26. case TextModes.CDATA:
  27. if (startsWith(s, ']]>')) {
  28. return true
  29. }
  30. break
  31. }
  32. return !s
  33. }

该函数的作用是在不同文本模式下,判断标签是否解析完毕。

参数说明,context 是解析上下文对象,拿到在 context.source 中的字符串;mode 文本模式,不同的模式结束方式不一样,需要区分判断;ancestors 解析的标签栈。

下面来看 switch 中对不同文本模式,是如何进行处理的。

如果文本模式为 TextModes.DATA,同时 startsWith(s, </),判断标签结束,看代码:

  1. case TextModes.DATA:
  2. if (startsWith(s, '</')) {
  3. for (let i = ancestors.length - 1; i >= 0; --i) {
  4. if (startsWithEndTagOpen(s, ancestors[i].tag)) {
  5. return true
  6. }
  7. }
  8. }
  9. break

从后向前找 ancestors 解析的标签栈,该结束标签名是否和栈中某个标签名相同,如果有相同的话,视为结束。

以上文本模式为 TextModes.DATA 判断过程,要求结束标签是以 </ 开头,既判断为结束,那该标签就解析完成了。

文本模式如果是 TextModes.RCDATA 或者 TextModes.RAWTEXT,看代码:

  1. case TextModes.RCDATA:
  2. case TextModes.RAWTEXT: {
  3. const parent = last(ancestors)
  4. if (parent && startsWithEndTagOpen(s, parent.tag)) {
  5. return true
  6. }
  7. break
  8. }

这两种模式,TextModes.RCDATA 模式元素 <textarea><title>TextModes.RAWTEXT 模式元素 <script><style>,要求开始标签必须存在,并且结束标签要和开始标签保持一致。

如果是 TextModes.CDATA 模式,很简单,结束必须是 ]]>

  1. case TextModes.CDATA:
  2. if (startsWith(s, ']]>')) {
  3. return true
  4. }
  5. break

满足以上的模式都视为结束当前解析。但如果很不幸没命中就结束,则在最后判断传入的字符串是否为空,不为空,继续解析;为空,则不需要解析了。

  1. return !s
1.3 解析规则

传入一个将要解析的模板字符串,会根据模板的前几个字符,来决定进入到那个解析函数进行处理。遇到不合规的字符串,则会抛出错误提示。

先来看解析规则源码:

  1. while (!isEnd(context, mode, ancestors)) {
  2. __TEST__ && assert(context.source.length > 0)
  3. const s = context.source
  4. let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
  5. // 文本、正常标签
  6. if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
  7. // 不在v-pre指令元素内,并且开始是 {{,也就是插值语法
  8. if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
  9. // '{{'
  10. // 处理插值语法,返回处理后的结果
  11. node = parseInterpolation(context, mode)
  12. } else if (mode === TextModes.DATA && s[0] === '<') { // 处理标签
  13. // 字符串长度为1个,也就是只有 < 不符合标签定义,抛出错误:意外的文本流
  14. if (s.length === 1) {
  15. emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
  16. } else if (s[1] === '!') { // 后面跟着感叹号 !
  17. if (startsWith(s, '<!--')) { // 解析注释
  18. node = parseComment(context)
  19. } else if (startsWith(s, '<!DOCTYPE')) { // 文档声明,当做注释节点
  20. node = parseBogusComment(context)
  21. } else if (startsWith(s, '<![CDATA[')) { //
  22. if (ns !== Namespaces.HTML) {
  23. node = parseCDATA(context, ancestors)
  24. } else {
  25. // 抛出错误,只允许在XML上下文中使用。
  26. emitError(context, ErrorCodes.CDATA_IN_HTML_CONTENT)
  27. // 当做注释节点
  28. node = parseBogusComment(context)
  29. }
  30. } else {
  31. // 不符合上面的规则,则抛出:错误的注释
  32. emitError(context, ErrorCodes.INCORRECTLY_OPENED_COMMENT)
  33. // 都当做注释解析
  34. node = parseBogusComment(context)
  35. }
  36. } else if (s[1] === '/') {
  37. if (s.length === 2) { // 只有两个字符 </
  38. // 抛出意外的文本流
  39. emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 2)
  40. } else if (s[2] === '>') { // 有结束,但没有标签名 </>
  41. // 抛错,预期的结束标签
  42. emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2)
  43. // 截取
  44. advanceBy(context, 3)
  45. // 解析</>没意义,直接跳过
  46. continue
  47. } else if (/[a-z]/i.test(s[2])) { // 第三个字符是字母,说明 isEnd 没有拦住为结束标签
  48. // 抛出错误,为无效的结束标签
  49. emitError(context, ErrorCodes.X_INVALID_END_TAG)
  50. // 把这个结束标签结束完毕
  51. parseTag(context, TagType.End, parent)
  52. // 无效的结束标签,没必要进入到节点对象,跳过
  53. continue
  54. } else {
  55. // 标签名不合法
  56. emitError(
  57. context,
  58. ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
  59. 2
  60. )
  61. // 解析为注释节点
  62. node = parseBogusComment(context)
  63. }
  64. } else if (/[a-z]/i.test(s[1])) { // 第二个字符是字母,则当做标签解析
  65. node = parseElement(context, ancestors)
  66. } else if (s[1] === '?') {
  67. // 抛错 <? 只能在 xml 中使用
  68. emitError(
  69. context,
  70. ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
  71. 1
  72. )
  73. // 当做注释节点
  74. node = parseBogusComment(context)
  75. } else {
  76. // 抛出:标签名不合法
  77. emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
  78. }
  79. }
  80. }
  81. // node为空,说明没有命中上面规则,则当做文本解析
  82. if (!node) {
  83. node = parseText(context, mode)
  84. }
  85. // 解析后放在集合中
  86. if (isArray(node)) {
  87. for (let i = 0; i < node.length; i++) {
  88. pushNode(nodes, node[i])
  89. }
  90. } else {
  91. pushNode(nodes, node)
  92. }
  93. }

上面的规则看似多,实际的主线以 < 开头,判断后面跟的字符,进入到解析函数中。

解析函数总结下来分为几类:

  1. parseInterpolation() 解析插值表达式。默认为双花括号 {{}}
  2. parseComment() 解析注释
  3. parseBogusComment() 解析文档声明
  4. parseTag() 解析标签
  5. parseElement() 解析元素节点,在内部执行解析标签 parseTag(),遇到嵌套子级会调用 parseChildren()
  6. parseText() 解析普通文本
  7. parseAttributeValue 解析行间属性值
  8. parseAttributes() 解析行间多个属性,内部会调用 parseAttribute()解析单个属性

每调用一个方法,解析完毕后,会返回解析后的 AST 对象,并把已经解析后的模板字符串截断。

解析的 AST 对象,会标记为不同的节点类型,方便在后续转换时,根据不同的类型做操作,例如解析到标签,则标记为 NodeTypes.ELEMENT,解析到属性时,标记为 NodeTypes.ATTRIBUTE

NodeTypes 的定义在 ast.ts 文件中,类型为 enum,实际上拿到的值为从0开始依次递增的数字:

  1. export const enum NodeTypes {
  2. ROOT, // 根节点 0
  3. ELEMENT, // 元素节点 1
  4. TEXT, // 文本节点 2
  5. COMMENT, // 注释节点 3
  6. SIMPLE_EXPRESSION, // 表达式 4
  7. INTERPOLATION, // 插值 {{}} 5
  8. ATTRIBUTE, // 属性 6
  9. DIRECTIVE, // 指令 7
  10. }

parseElement 解析节点元素

命中该解析函数的规则,是以 < 开头,后面跟上一个字母。

我们先从解析元素标签开始去分析。【需要张图】

  1. function parseElement(
  2. context: ParserContext,
  3. ancestors: ElementNode[]
  4. ): ElementNode | undefined {
  5. // 判断字符串开头必须是以 < 开头,后面跟着一个字母,忽略大小写
  6. __TEST__ && assert(/^<[a-z]/i.test(context.source))
  7. // Start tag.
  8. // 是否在 pre 标签内
  9. const wasInPre = context.inPre
  10. // 是否在有 v-pre 属性的标签内
  11. const wasInVPre = context.inVPre
  12. // 拿到父级
  13. const parent = last(ancestors)
  14. // 开始解析标签
  15. const element = parseTag(context, TagType.Start, parent)
  16. const isPreBoundary = context.inPre && !wasInPre
  17. const isVPreBoundary = context.inVPre && !wasInVPre
  18. // 自闭合标签,或者调用解析配置项来判断
  19. if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
  20. return element
  21. }
  22. // 存入栈中
  23. // Children.
  24. ancestors.push(element)
  25. // 拿到元素的模式文本模式
  26. const mode = context.options.getTextMode(element, parent)
  27. // 继续解析子级
  28. const children = parseChildren(context, mode, ancestors)
  29. // 子级解析完毕后,从栈中推出
  30. ancestors.pop()
  31. // 存子级
  32. element.children = children
  33. // 剩下结束标签
  34. // End tag.
  35. if (startsWithEndTagOpen(context.source, element.tag)) {
  36. // 解析结束标签
  37. parseTag(context, TagType.End, parent)
  38. } else {
  39. // 报错“元素没有结束标签”
  40. emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
  41. if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
  42. const first = children[0]
  43. if (first && startsWith(first.loc.source, '<!--')) {
  44. emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)
  45. }
  46. }
  47. }
  48. // 选取解析范围的字符串和位置信息
  49. element.loc = getSelection(context, element.loc.start)
  50. // // 不在在 pre 中,设置为false
  51. if (isPreBoundary) {
  52. context.inPre = false
  53. }
  54. // 不在 v-pre 指令标签内,设置为 false
  55. if (isVPreBoundary) {
  56. context.inVPre = false
  57. }
  58. // 返回元素对象
  59. return element
  60. }

这个函数实际上做的事就是调度解析标签的其他方法,我们知道,一个完整的标签,包含标签名、行间属性、子级结构(闭合标签除外)。

解析标签名和行间属性交给了 parseTag 去做,稍后再详细分析该函数,这里看下接收的参数。

  1. const element = parseTag(context, TagType.Start, parent)

参数说明,context 为解析上下文对象;TagType.Start 是一个标识,代表传入标签类型是开始标签,后面解析结束标签也会调用这个方法,传入的是TagType.End 标识; parent 是解析当前标签的父级,通过 last(ancestors) 获取解析标签栈的最后一个元素。

返回值是一个解析后的元素对象。

元素标签按照闭合的方式,有自闭合标签(self-closing)和非闭合标签之分,自闭合标签如 <input /><img /> 等,这类没有子元素;非闭合标签,如 <div></div><span></span>,这类标签是双标签形式,之间可以包含子元素。如果有子元素的情况,还要继续解析下去。

判断闭合标签,通过解析后的元素对象属性判定,或者通过传入的解析配置项来判定:

  1. // 自闭和标签,或者调用辅助方法来判定
  2. if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
  3. return element
  4. }

是闭合标签,没有结束标签,到这里标签解析完毕了,返回解析后的元素对象即可。

非闭合标签,要有解析子元素的操作,此时标签还没解析完毕,需要先将当前元素对象存入到栈中,然后走解析子结构流程,等子结构解析完毕,当前标签也就解析完毕了,从栈中弹出。

代码:

  1. // 子级没解析完毕,存入栈中,解析子级时会用
  2. ancestors.push(element)
  3. // 拿到元素的文本模式
  4. const mode = context.options.getTextMode(element, parent)
  5. // 解析子级
  6. const children = parseChildren(context, mode, ancestors)
  7. // 子级解析完毕后,从栈中推出
  8. ancestors.pop()

解析子级结构进入到 parseChildren,通过 last 函数拿到栈中最后一个元素对象,其实就是未解析完毕的父级,通过这段代码可以清晰的理解。进入 parseChildren 后字符串依然要命中解析规则,来决定执行解析函数。

调用解析配置项来得到当前结构的文本模式,默认都是 TextModes.DATA 。在浏览器平台,标签为 textareatitle,文本模式为 TextModes.RCDATA;标签为 style,iframe,script,noscript 文本模式为 TextModes.RAWTEXT;其他的都是 TextModes.DATA

parseTag 解析标签

无论是开始标签还是结束标签,都需要此函数进行解析,在调用时需要告知函数要解析的是开始还是结束标签。

先看代码代码的参数和返回值代码:

  1. function parseTag(
  2. context: ParserContext,
  3. type: TagType,
  4. parent: ElementNode | undefined
  5. ): ElementNode {
  6. // 获取解析前的位置信息,将来通过这个信息和解析后的信息,找出该标签在源模板中字符串
  7. const start = getCursor(context)
  8. // 匹配标签名称
  9. const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
  10. const tag = match[1]
  11. // 获取标签的命名空间
  12. const ns = context.options.getNamespace(tag, parent)
  13. // 标签名字解析后,模板字符串向前移动,标签名称个长度
  14. advanceBy(context, match[0].length)
  15. // 删除匹配的空格,向后移动
  16. advanceSpaces(context)
  17. // 得到一份位置信息,目的是在下面遇到 pre 时, 会重新解析标签,重置到这个位置
  18. const cursor = getCursor(context)
  19. // 存一份解析的模板字符串,方便下面重置时用
  20. const currentSource = context.source
  21. // 解析行间属性
  22. let props = parseAttributes(context, type)
  23. // 调用选项辅助方法,检查是否是 pre 标签
  24. if (context.options.isPreTag(tag)) {
  25. context.inPre = true
  26. }
  27. // 如果不在 pre 标签内,同时存在指令 v-pre,则要重新解析一遍行间属性
  28. if (
  29. !context.inVPre &&
  30. props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
  31. ) {
  32. // 标记为 true,方便继续解析时,判断是在 v-pre 标签内
  33. context.inVPre = true
  34. // 重置解析字符串的位置
  35. extend(context, cursor)
  36. context.source = currentSource
  37. // 重新解析一遍 pre 中的行间属性,把行间有 v-pre 指令过滤掉
  38. props = parseAttributes(context, type).filter(p => p.name !== 'v-pre')
  39. }
  40. // 是否是自闭合标签
  41. let isSelfClosing = false
  42. // 标签解析完毕,剩余的模板字符串空了,说明没有结束标签,提醒错误
  43. if (context.source.length === 0) {
  44. emitError(context, ErrorCodes.EOF_IN_TAG)
  45. } else {
  46. // 如果剩下的模板字符串,是以 "/>" 开头,判定为自闭合标签
  47. isSelfClosing = startsWith(context.source, '/>')
  48. // 如果传入的是要解析双标签的结束标签,却遇到了是自闭合标签,则提醒错误
  49. if (type === TagType.End && isSelfClosing) {
  50. emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
  51. }
  52. // 截断字符传,自闭合标签向后截取两位,以为是 /> ,不是的话截一位 是 >
  53. advanceBy(context, isSelfClosing ? 2 : 1)
  54. }
  55. let tagType = ElementTypes.ELEMENT
  56. const options = context.options
  57. // 不在 pre 中,不是自定义标签
  58. if (!context.inVPre && !options.isCustomElement(tag)) {
  59. // 某些属性是否有 v-is指令
  60. const hasVIs = props.some(
  61. p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
  62. )
  63. // 判断原生标签方法存在,并且没有v-is指令
  64. if (options.isNativeTag && !hasVIs) {
  65. // 不是原生标签,则被标记为组件标签
  66. if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
  67. } else if (
  68. hasVIs || // 有 v-is指令
  69. isCoreComponent(tag) || // 是内置的组件名称
  70. (options.isBuiltInComponent && options.isBuiltInComponent(tag)) || // 如果辅助方法判断是内置标签
  71. /^[A-Z]/.test(tag) ||
  72. tag === 'component' // 或标签名为 "component"
  73. ) {
  74. tagType = ElementTypes.COMPONENT // 类型设置为组件
  75. }
  76. // 如果是 slot 标签
  77. if (tag === 'slot') {
  78. tagType = ElementTypes.SLOT
  79. } else if (
  80. tag === 'template' &&
  81. props.some(p => {
  82. return (
  83. p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
  84. )
  85. })
  86. ) {
  87. tagType = ElementTypes.TEMPLATE
  88. }
  89. }
  90. return {
  91. type: NodeTypes.ELEMENT, // 标签类型
  92. ns, // 命名空间
  93. tag, // 标签名称
  94. tagType, // 标签名称类型
  95. props, // 行间属性集合
  96. isSelfClosing, // 是否是自闭合标签
  97. children: [], // 子级
  98. loc: getSelection(context, start), // 当前标签在源模板字符串的位置信息
  99. codegenNode: undefined
  100. }
  101. }

参数说明,context 是解析上下文对象,type 是指定解析的标签是开始还是结束标签,有两个值 TagType.Start,或者是以 parent 解析当前标签的父级对象。

返回值是一个元素节点,类型都为 NodeTypes.ELEMENT。根据标签名字的不同,将标签再进行细分,存在 tagType 中,是标签的类型,有以下几种,以及满足什么样的条件:

  • ElementTypes.ELEMENT 元素标签。除了下面三种标签外,都属于这个类型。
  • ElementTypes.COMPONENT 组件标签,以下标记为此类型:
    • 不是原生的标签,例如不是HTML的 div、span、a 这些,可通过选项辅助方法 isNativeTag 来指定
    • 行间属性有 v-is 指令
    • 是内置的组件,如:Teleport、Suspense、KeepAlive、BaseTransition
    • 是指定的内置组件,可通过选项辅助方法 isBuiltInComponent 来指定
    • 开头名字是大写
    • 标签名字为 component
  • ElementTypes.SLOT slot标签。tag名字为 slot 。
  • ElementTypes.TEMPLATE 模板标签。标签名字为 template 并且行间属性名称有是指定的模板指令:if,else,else-if,for,slot

匹配标签名

通过正则来匹配标签名。

  1. // 开始标签
  2. const source = '<div test="1"></div>'
  3. const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(source)
  4. console.log(match); // 打印:["<div", "div"]
  5. // 结束标签
  6. const source = '</div>'
  7. const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(source)
  8. console.log(match); // 打印:["</div", "div"]

开头必须以 < 开头。后面 /,可以有,可以没有,因为此函数是用来解析标签的,不分前后标签,也就是 \

\
,解析到开始和结束标签时,都会走到这里。后面小括号分组了,是用来拿到分组的标签名称,名称为 a-z 的字母,忽略大小写,直到遇到制表符(\t)、回车(\r)、换行(\n)、换页(\f)、空格( )、斜杠(/),闭合(>)一个时,就停下来。正则的方法 exec返回一个数组,通过 match[1],就能拿到标签名字。

匹配完之后向后移动,具体参考:

解析行间属性

解析行间属性比较复杂,调用函数 parseAttributes,进行解析,这个后面会降到。行间属性可以存在多个,返回是一个数组形式。

判断是否是 pre 标签

pre 标签可定义预格式化的文本,被包围在 \ 标签 元素中的文本通常会保留空格和换行符。在 pre 标签内的插值运算只当成文本处理,而不会解析为表达式。

例如模板:

  1. const source = '<pre> {{hello}} <div>hello</div> </pre>'

显示在浏览器中时,还保留双大括号,而不是作为插值表达式解析。

一单判定为 pre 标签,就会把解析上下文对象 contextinPre 设置为 true,这样在解析到 pre 包含的子级时,就会做特殊处理了。当 pre 解析完毕,又会把 inPre 设置为 false,具体可以看上面 parseElement 函数的注释。

调用选项辅助方法 isPreTag,来判断是否是 pre 标签,在浏览器就是 pre 这个名字,在别的平台可能是别的名字,可通过选项辅助方法 isPreTag 自己定义。

自闭合标签处理

有时会遇到是自闭合标签,这样向后截取的位数是不同的,例如解析 \\\img 是自闭合标签,解析完要向后截取2个位置才到标签 a。非闭合标签 a,结束为 >,只需要向后截取1个位置。

如果遇到异常情况,则需要提醒,具体看代码注释。

parseAttributes 解析多个行间属性

这个函数相对来说比较简单。

  1. function parseAttributes(
  2. context: ParserContext,
  3. type: TagType
  4. ): (AttributeNode | DirectiveNode)[] {
  5. // 存放多个解析后的行间属性
  6. const props = []
  7. // 存放行间属性,去重用
  8. const attributeNames = new Set<string>()
  9. while (
  10. context.source.length > 0 && // 要解析的字符串不能为空
  11. !startsWith(context.source, '>') && // 字符串开头不能是 > 。遇到 > 说明行间属性解析完毕了
  12. !startsWith(context.source, '/>') // 也不能是 /> ,这也是解析完毕
  13. ) {
  14. if (startsWith(context.source, '/')) { // 如果字符串以 / 开头,提示非法的,并向后截取1位,不进行解析了
  15. emitError(context, ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG)
  16. advanceBy(context, 1)
  17. advanceSpaces(context)
  18. continue
  19. }
  20. // 如果要解析的是结束标签,则提醒:“结束标签不能有行间属性”
  21. if (type === TagType.End) {
  22. emitError(context, ErrorCodes.END_TAG_WITH_ATTRIBUTES)
  23. }
  24. // 开始解析行间属性
  25. const attr = parseAttribute(context, attributeNames)
  26. // 只有开始标签,才存解析后的行间属性
  27. if (type === TagType.Start) {
  28. props.push(attr)
  29. }
  30. // 解析完一个属性后,遇到正则中的字符,则是在预期中,这其实就是个提醒
  31. if (/^[^\t\r\n\f />]/.test(context.source)) {
  32. emitError(context, ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES)
  33. }
  34. // 将空白消除掉
  35. advanceSpaces(context)
  36. }
  37. // 最后返回解析后的属性数组
  38. return props
  39. }

接收参数说明,context 为解析上线文对象;type 为开始或结束标签的标识。

返回值是一个数组,存的是通过 parseAttribute 函数解析后的行间属性对象形式,这个稍后进函数再看。

定义了个 Set 数据结构,用来存已经解析的行间属性,如果定义了相同行间属性名称,则会报错,具体会在 parseAttribute 中判断。

行间属性可以写多个,用了 while 循环挨个遍历解析,直到解析的字符串为空了,同时解析到了 >/>,被视为行间属性解析完毕。

只有当 typeTagType.Start 时,也就是开始标签时,行间属性才有意义,如果为结束标签,写在结束标签内的属性没有意义。

parseAttribute 解析行间属性

这个函数是用来解析行间属性。

可以看到只有当传入为字符串 template 时,才调用 baseParse 方法进行解析,返回是 AST 对象。options 为解析过程中,根据不同平台传入的辅助属性或方法,这个到 baseParse 函数中来看具体可以传入什么样的辅助属性。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注