vue3 compile系列一:parse

1,262 阅读5分钟

compile的第一步,如果模版template传入的是一个字符串类型,那么就需要用到parse模块,将template转化成ast。

其中,options是CompilerOptions类型,是ParseOptionsTransformOptionsCodegenOptions的交叉类型。options作为compile的入参,携带了parse、transform、codegen一系列过程的参数。

barseParsepackages/compiler-core/src/parse.ts中导出的方法,根据模版opitons参数最终生成一棵抽象语法树,并返回根结点。

具体看下baseParse的解析过程:

1、生成上下文

createParserContext(content, options)生成context上下文。

function createParserContext(content: string, rawOptions: ParserOptions): ParserContext {
  const options = extend({}, defaultParserOptions)
  for (const key in rawOptions) {   // 合并用户rawOptions和defaultParserOptions到options中
    // @ts-ignore
    options[key] = rawOptions[key] || defaultParserOptions[key]
  }
  return {
    options,   // 
    column: 1,    // 初始列
    line: 1,     // 初始行
    offset: 0,   // 初始偏移量
    originalSource: content,  // 原始template文本内容,readonly
    source: content,  // 当前剩余未解析内容
    inPre: false,  // HTML <pre> tag, preserve whitespaces
    inVPre: false  // v-pre, do not process directives and interpolations
  }
}

返回的options和defaultParserOptions都是ParseContext类型,其中optionsMergedParserOptions类型。

type OptionalOptions = 'isNativeTag' | 'isBuiltInComponent'
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
  Pick<ParserOptions, OptionalOptions>

ParserOptions中的属性存在在OptionalOptions中的类型保持不变,不存在的可选属性变成必须属性,其他保持不变。

2. 生成游标

function getCursor(context: ParserContext): Position {
  const { column, line, offset } = context
  return { column, line, offset }
}

3. 生成并返回ast

createRoot返回ast的根结点,传入childrenloc,并初始化其他属性。那么,childrenloc就是每个ast不同于其他的地方。

3.1 loc

locgetSelection(context, start)生成,其中context为初始上下文,start为初始位置。

getSelection方法比较简单,就是获取根据传入的上下文和始末位置,截取内容。在createRoot方法中,locstartend都为初始位置,source为空。

3.2 parseChildren

parseChildren(context, TextModes.DATA, [])

parseChildren大概有一百多行代码,先大概看一下:

1). 初始化parentnsnodes

2). while循环,isEnd根据不同的mode,判断context.source是否命中最临近祖先结点的闭标签。如果是根节点,modeTextModes.DATA, ancestors为空,while循环一直查找到context.source为空,即整个template全部遍历完

3). 根据条件判断,是否需要移除多余的空格注释

4). 将nodes结点数组返回

其中,最核心的是第2)步,将2)拆解开,具体看一下:

const s = context.source
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
  • 1 modeTextModes.DATA, TextModes.RCDATA

  •     1.1 s以"{{"开头,即插值,并且context.inVPre为假,即可以处理插值

    node = parseInterpolation(context, mode)
    
  • 1.2 mode为TextModes.DATA, s[0]是"<"

  •          1.2.1 s的长度为1

    emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)  // 调用错误处理方法,context的offset和colume分别加1
    
  • 1.2.2 s[1]是"!"

  •               1.2.2.1 s以"<!--"开头,即注释

    node = parseComment(context)
    
  •               1.2.2.2 s以"<!DOCTYPE"开头,即文档类型 

    node = parseComment(context)
    
  •               1.2.2.3 s以"<!CDATA["开头,即纯文本

  •                    1.2.2.3.1 ns !== Namespaces.HTML

    node = parseCDATA(context, ancestors)
    
  •                    1.2.2.3.2 ns === Namespaces.HTML,

    emitError(context, ErrorCodes.CDATA_IN_HTML_CONTENT)
    node = parseBogusComment(context)
    
  •          1.2.3 s[1]是"/"

  •                    1.2.3.1 s的长度为2,

    emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 2)  // 调用错误处理方法,context的offset和colume分别加2
    
  •                    1.2.3.2 s[2]是">", 

    emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2) // 调用错误处理方法,context的offset和colume分别加2
    advanceBy(context, 3) // 重新计算游标位置,这里可以思考下为什么上边的其他情况只报错,没有游标处理?(因为不牵扯换行,emitError方法里只对offset和column做了更新)
    
  • 1.2.3.3 s[2]是"a-z"中的一个,

    emitError(context, ErrorCodes.X_INVALID_END_TAG) parseTag(context, TagType.End, parent)

  •                   1.2.3.4 其他情况

    emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 2)
    node = parseBogusComment(context)
    
  •          1.2.4 s[1]是"a-z"中的一个,

    node = parseElement(context, ancestors)
    
  •          1.2.5 s[1]是"?",

    emitError(context, ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME, 1)
    node = parseBogusComment(context)
    
  •          1.2.6 s[1]是其他,

    emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
    
  • 2 如果不是插值,注释或者element,

    node = parseText(context, mode)
    

最后,根据node是否是数组(parseElement的结果),将node push到结果数组nodes中。

这就是parse的主要过程,具体调用的元素解析方法包括

  • parseComment

  • parseBogusComment

  • parseElement

  • parseTag

  • parseAttributes

  • parseAttribute

  • parseAttributeValue

  • parseInterpolation

  • parseText

  • parseTextData

篇幅有限,可以自行查阅源码。其中,比较特殊的是parseElement,用到了parseTagparseAttributes获取结点的标签、属性(基本都是通过正则表达式匹配实现),并且还用到了parseChildren的递归解析子结点。

parseTag

1)判断context.source是不是以合法的标签或者闭标签开头,type是不是TagType.Start或者TagType.End(根据环境不同,是可选的步骤)

2)获取游标起始位置,tag标签名,命名空间,游标向后移动标签长度,更新context上下文,过滤更新后的context.source中的空格

3)保存此时的游标和上下文

4)parseAttributes(context, type)得到props

5)判断tag是不是<pre>标签

6)判断props中是否包含v-pre指令,如果包含,将parseAttributes更新的上下文和游标重置到步骤3)保存的位置,重新parseAttributes(context, type)并且过滤掉v-pre(不知道为什么要重新解析attributes的同学,可以先了解下v-pre的含义,这里可以理解成属性绑定,插值等都不做替换,当前结点及子节点当做静态结点处理)

7)来到了标签结束位置,如果是自闭合标签,游标向后移动2位,不是的话,移动1

8)确定tagType;初始化为ElementTypes.ELEMENT;判断是否包含is绑定,根据结果和tag的类型,得到tagType的值(ElementTypes.COMPONENTElementTypes.SLOTElementTypes.TEMPLATE中的一个)

9)返回ElementType的实例

了解了parseTag的过程之后,就可以看下parseElement了

parseElement

parseElement的过程也比较清晰,主要是三步

1)start tag

  • 获取当前上下文的inPre、inVPre,获取parent结点
  • 调用parseTag得到element
  • 重新获取parseTag后上下文的inPre、inVPre
  • 如果element是自闭合或者void标签,直接将element返回

2)children

将1)的element作为parent,递归调用parseChildren解析子节点

3)end tag

  • 调用parseTag解析闭合标签
  • 更新loc属性,将1)暂存的inPre、inVPre重新赋值给上下文,相当于初始化,为下一次parse准备

这样,parse的过程基本就清晰了,就是逐行解析string类型的模板,将它翻译成vue能识别的标签和指令,得到的ast会作为接下来transform的参数,继续转化的过程。

可以在vue3提供的模板编译环境的控制台,看下生成的ast来理解parse的过程。