compile的第一步,如果模版template传入的是一个字符串类型,那么就需要用到parse模块,将template转化成ast。
其中,options是CompilerOptions类型,是ParseOptions、TransformOptions、CodegenOptions的交叉类型。options作为compile的入参,携带了parse、transform、codegen一系列过程的参数。
barseParse是packages/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类型,其中options是MergedParserOptions类型。
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的根结点,传入children和loc,并初始化其他属性。那么,children和loc就是每个ast不同于其他的地方。
3.1 loc
loc由getSelection(context, start)生成,其中context为初始上下文,start为初始位置。
getSelection方法比较简单,就是获取根据传入的上下文和始末位置,截取内容。在createRoot方法中,loc的start和end都为初始位置,source为空。
3.2 parseChildren
parseChildren(context, TextModes.DATA, [])
parseChildren大概有一百多行代码,先大概看一下:
1). 初始化parent、ns、nodes
2). while循环,isEnd根据不同的mode,判断context.source是否命中最临近祖先结点的闭标签。如果是根节点,mode为TextModes.DATA, ancestors为空,while循环一直查找到context.source为空,即整个template全部遍历完
3). 根据条件判断,是否需要移除多余的空格和注释
4). 将nodes结点数组返回
其中,最核心的是第2)步,将2)拆解开,具体看一下:
const s = context.source
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
-
1
mode为TextModes.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,用到了parseTag,parseAttributes获取结点的标签、属性(基本都是通过正则表达式匹配实现),并且还用到了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.COMPONENT、ElementTypes.SLOT、ElementTypes.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的过程。