一、编译流程
1. 解读入口文件 packgages/vue/index.ts
首先从Vue对象的入口开始,packgages/vue/index.ts文件中只有compileToFunction函数:
<template>
<div>
Hello World
</div>
</template>
经过编译后,code返回的字符串为:
const _Vue = Vue return function render(_ctx, _cache) {
with(_ctx) {
const {
openBlock: _openBlock, createBlock:_createBlock
} = _Vue;
return (_openBlock(), _createBlock("div", null, "Hello World"))
}
}
2. compile函数的运行流程
compile函数涉及到compile-dom 和compile-core 两个模块。
compile的运行流程:
baseCompile命名理由:因为compile-core是编译的核心模块,接收外部的参数来按照规则完成编译,而compile-dom是专门处理浏览器场景下的编译,在这个模块下导出的compile函数是入口文件真正接收的编译函数。而compile-dom中的compile函数相对baseCompile也是一个更高阶的编译器。例如:当Vue在weex或iOS或Android这些Native App中工作时,compile-dom可能会被相关的移动端编译库来取代。baseCompile函数:
- 从函数声明中看,baseCompile接收template模版以及上层高阶编译器处理过的options编译选项,最终返回一个CodegenResult类型的编译结果。
export interface CodegenResult {
code: string
preamble: string
ast: RootNode
map?: RawSourceMap
}
- 看上方源码的第12行,判断template模版是否为字符串,如果是的话,则会对字符串进行解析,否则直接将template作为AST。(我们平时写的vue代码都是以字符串的形式传递进去的。)
- 然后是第16行调用了transform函数,以及传入了指令转换、节点等工具函数,对由模版生成的AST进行转换。
- 最后32行,将转换好的ast传入进generate,生成CodegenResult类型的返回结果。
二、AST 解析器
1. ast 的生成
ast的生成有一个三目运算符的判断,如果传进来的template模版是一个字符串,那么则调用baseParse解析模版字符串,否则直接将template作为ast对象。
baseParse 函数:
export function baseParse(
content: string,
options: ParserOptions = {}
): RootNode {
const context = createParserContext(content, options)
const start = getCursor(context)
return createRoot(
parseChildren(context, TextModes.DATA, []),
getSelection(context, start)
)
}
- 首先会创建解析的上下文,根据上下文获取游标信息,由于还未进行解析,所以游标中的
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
}
}
- 该函数返回了一个
RootNode类型的根节点对象,其中我们传入的children参数会被作为根节点的children参数。
3. 解析子节点 parseChildren(关键)
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数组。
while循环内的逻辑(函数的核心):
- 在while中会判断文本数据的类型,只有当TextModes为DATA或RCDATA时会继续往下解析。
- 第一种情况就是判断是否需要解析Vue模版语法中的
Mustache语法,如果当前上下文中没有v-pre指令来跳过表达式,并且源模版字符串是以我们指定的分隔符开头的,就会进行双大括号的解析。 - 接下来会判断,如果第一个字符是
<并且第二个字符是! ,会尝试解析注释标签,<!DOCTYPE>和<!CDATA这三种情况,对于DOCTYPE会进行忽略,解析成注释。 - 之后会判断当第二个字符是
/的情况,</已经满足了一个闭合标签的条件了,所以会尝试匹配闭合标签。当第三个标签是>,缺少了标签名字,会报错,并让解析器的进度前进三个字符,跳过</>。 - 如果是
</,并且第三个字符是小写英文字符,解析器会解析结束标签。 - 如果源模版字符串的第一个字符是
<,第二个字符是小写英文字符开头,会调用parseElement函数来解析对应的标签。 - 当这个判断字符串字符的分支条件结束,并且没有解析出任何node节点,则会将node作为文本类型,调用parseText进行解析。
- 最后将生成的节点添加进nodes数组,在函数结束时返回。
while循环的源码如下:
while (!isEnd(context, mode, ancestors)) {
const s = context.source
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0] === '<') {
if (s[1] === '!') {
if (startsWith(s, '<!--')) {
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) {
node = parseBogusComment(context)
} else if (startsWith(s, '<![CDATA[')) {
if (ns !== Namespaces.HTML) {
node = parseCDATA(context, ancestors)
}
}
} else if (s[1] === '/') {
if (s[2] === '>') {
emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2)
advanceBy(context, 3)
continue
} else if (/[a-z]/i.test(s[2])) {
parseTag(context, TagType.End, parent)
continue
} else {
node = parseBogusComment(context)
}
} else if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors)
} else if (s[1] === '?') {
node = parseBogusComment(context)
} else {
emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
}
}
}
if (!node) {
node = parseText(context, mode)
}
if (isArray(node)) {
for (let i = 0; i < node.length; i++) {
pushNode(nodes, node[i])
}
} else {
pushNode(nodes, node)
}
}
4. 解析模版元素 Element
parseElement精简源码如下:
function parseElement(
context: ParserContext,
ancestors: ElementNode[]
): ElementNode | undefined {
const parent = last(ancestors)
const element = parseTag(context, TagType.Start, parent)
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element
}
ancestors.push(element)
const mode = context.options.getTextMode(element, parent)
const children = parseChildren(context, mode, ancestors)
ancestors.pop()
element.children = children
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End, parent)
} else {
emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
const first = children[0]
if (first && startsWith(first.loc.source, '<!--')) {
emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)
}
}
}
element.loc = getSelection(context, element.loc.start)
return element
}
- 首先会获取当前节点的父节点,再调用
parseTag()函数解析。
parseTag()函数执行流程:
- 匹配标签名
- 解析元素中的attribute属性,存储至props属性
- 检测是否存在v-pre属性,如果存在,则修改context上下文中的inVPre属性为true。
- 检测自闭合标签,如果是自闭合,则将isSelfClosing属性置为true。
- 判断tagType,是Element还是component组件,或slot插槽。
- 返回生成的element对象
- 获取到 element对象后,会判断element是否是自闭合标签,或空标签,例如
<img>、<br>、<hr>,如果是这种情况,直接返回element对象。 - 然后解析element的子节点,把element压入栈中,然后递归调用parseChildren来解析子节点。
const parent = last(ancestors)
在将element入栈后,拿到的父节点就是当前节点。
- 解析完毕后,调用
ancestors.pop(),让当前解析完子节点的element对象出栈,将解析后的children对象赋值给element的children属性,完成element的子节点解析。 - 最后匹配结束标签,设置element的Ioc位置信息,返回解析完毕的 element 对象。
模版元素解析-举例分析
<div>
<p>Hello World</p>
</div>
- div 入栈,解析div的子节点,将p标签压入栈中
- 解析出文本节点:Hello World,放入p节点的children中,p.children = [‘Hello World’]
- p节点没有其余子节点,解析完成,出栈。div.children = [p]
- div 没有其余子节点,匹配完结束标签后,返回解析结果,存放祖先节点的栈清空。
三、静态提升
什么是静态提升?
- 当Vue的编译器在编译过程中,发现了一些不会变的节点或属性,就会给这些节点打上标记。然后编译器在编译器过程中,发现了一些不会变的节点或属性,就会给这些节点打上标记。
- 编译器在生成代码字符串的过程中,会发现这些静态的节点,并提升它们,将他们序列化成字符串,以此减少编译及渲染成本。有时可以跳过一整颗树。