vue3源码学习10-编译one-模板解析为AST
首先找到编译的入口compile函数:
// 支持两个参数,第一个template是待编译的模板字符串,第二个options是编译的一些配置信息
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
return baseCompile(
template,
extend({}, parserOptions, options, {
nodeTransforms: [
// ignore <script> and <tag>
// this is not put inside DOMNodeTransforms because that list is used
// by compiler-ssr to generate vnode fallback branches
ignoreSideEffectTags,
...DOMNodeTransforms,
...(options.nodeTransforms || [])
],
directiveTransforms: extend(
{},
DOMDirectiveTransforms,
options.directiveTransforms || {}
),
transformHoist: __BROWSER__ ? null : stringifyStatic
})
)
}
可以发现内部执行的是baseCompile方法,而且在options的基础上又扩展了一些参数,下来看一下baseCompile的实现:packages/compiler-core/src/compile.ts
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module'
/* istanbul ignore if */
if (__BROWSER__) {
if (options.prefixIdentifiers === true) {
onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
} else if (isModuleMode) {
onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
}
}
const prefixIdentifiers =
!__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
if (!prefixIdentifiers && options.cacheHandlers) {
onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED))
}
if (options.scopeId && !isModuleMode) {
onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
}
// 解析template生成AST
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers)
if (!__BROWSER__ && options.isTS) {
const { expressionPlugins } = options
if (!expressionPlugins || !expressionPlugins.includes('typescript')) {
options.expressionPlugins = [...(expressionPlugins || []), 'typescript']
}
}
// AST 转换
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
// 生成代码
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
AST(以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构),这里只要知道AST中的节点是可以完整描述她在模板中映射的节点信息,一颗必须有根节点,它的根节点是一个虚拟节点,并不会映射到一个具体节点,这里的虚拟节点就可以让我们在template中定义多个根节点,这个在vue2.x中是会报错的。现在看下解析template生成的实现:
packages/compiler-core/src/parse.ts
export function baseParse(
content: string,
options: ParserOptions = {}
): RootNode {
// 创建解析上下文
const context = createParserContext(content, options)
const start = getCursor(context)
// 解析子节点,并创建AST
return createRoot(
parseChildren(context, TextModes.DATA, []),
getSelection(context, start)
)
}
// 默认解析配置
export const defaultParserOptions: MergedParserOptions = {
delimiters: [`{{`, `}}`],
getNamespace: () => Namespaces.HTML,
getTextMode: () => TextModes.DATA,
isVoidTag: NO,
isPreTag: NO,
isCustomElement: NO,
decodeEntities: (rawText: string): string =>
rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
onError: defaultOnError,
onWarn: defaultOnWarn,
comments: __DEV__
}
// 1.创建解析上下文
function createParserContext(
content: string,
rawOptions: ParserOptions
): ParserContext {
const options = extend({}, defaultParserOptions)
let key: keyof ParserOptions
for (key in rawOptions) {
// @ts-ignore
options[key] =
rawOptions[key] === undefined
? defaultParserOptions[key]
: rawOptions[key]
}
return {
// options表示解析的相关配置
options,
// 当前代码列号
column: 1,
// 当前代码行号
line: 1,
// 当前代码相对于原始代码的偏移量
offset: 0,
// 最初的原始代码
originalSource: content,
// 当前代码
source: content,
// 当前代码是否在pre标签内
inPre: false,
// 当前代码是否在v-pre指令环境下
inVPre: false,
onWarn: options.onWarn
}
}
// 2.解析子节点: 解析并创建AST节点数组,一是自顶向下分析代码,生成AST节点数组;二是空白字符管理,提高编译的效率。
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)) {
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] === '<') {
// https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
// 处理 < 开头的代码
if (s.length === 1) {
// s 长度为1, 说明代码结尾是< ,报错
emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
} else if (s[1] === '!') {
// 处理 <! 开头的代码
// https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state
if (startsWith(s, '<!--')) {
// 处理注释节点
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) {
// Ignore DOCTYPE by a limitation.
// 处理 <!DOCTYPE 节点
node = parseBogusComment(context)
} else if (startsWith(s, '<![CDATA[')) {
// 处理 <![CDATA[ 节点
if (ns !== Namespaces.HTML) {
node = parseCDATA(context, ancestors)
} else {
emitError(context, ErrorCodes.CDATA_IN_HTML_CONTENT)
node = parseBogusComment(context)
}
} else {
emitError(context, ErrorCodes.INCORRECTLY_OPENED_COMMENT)
node = parseBogusComment(context)
}
} else if (s[1] === '/') {
// https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
// 处理 </ 结束标签
if (s.length === 2) {
// 长度为2 说明结尾是 </ ,报错
emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 2)
} else if (s[2] === '>') {
// </> 缺少结束标签,报错
emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2)
advanceBy(context, 3)
continue
} else if (/[a-z]/i.test(s[2])) {
// 多余的结束标签
emitError(context, ErrorCodes.X_INVALID_END_TAG)
parseTag(context, TagType.End, parent)
continue
} else {
emitError(
context,
ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
2
)
node = parseBogusComment(context)
}
} else if (/[a-z]/i.test(s[1])) {
// 解析标签元素节点
node = parseElement(context, ancestors)
} else if (s[1] === '?') {
emitError(
context,
ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
1
)
node = parseBogusComment(context)
} else {
emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
}
}
}
if (!node) {
// 解析普通文本节点
node = parseText(context, mode)
}
if (isArray(node)) {
// 如果 node 是数组,则遍历添加
for (let i = 0; i < node.length; i++) {
pushNode(nodes, node[i])
}
} else {
// 添加单个node
pushNode(nodes, node)
}
}
// Whitespace handling strategy like v2
// 模板代码中经常会出现换行和空格,需要移除这些没有意义的节点,提高编译效率
let removedWhitespace = false
if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {
const shouldCondense = context.options.whitespace !== 'preserve'
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (!context.inPre && node.type === NodeTypes.TEXT) {
if (!/[^\t\r\n\f ]/.test(node.content)) {
// 匹配空白字符
const prev = nodes[i - 1]
const next = nodes[i + 1]
// Remove if:
// - the whitespace is the first or last node, or:
// - (condense mode) the whitespace is adjacent to a comment, or:
// - (condense mode) the whitespace is between two elements AND contains newline
// 如果空白字符是开头或者结尾节点, 或者空白字符与注释节点相连,或者空白字符在两个元素之间并包含换行符,直接移除
if (
!prev ||
!next ||
(shouldCondense &&
(prev.type === NodeTypes.COMMENT ||
next.type === NodeTypes.COMMENT ||
(prev.type === NodeTypes.ELEMENT &&
next.type === NodeTypes.ELEMENT &&
/[\r\n]/.test(node.content))))
) {
removedWhitespace = true
nodes[i] = null as any
} else {
// 否则压缩这些空白字符成一个空格
// Otherwise, the whitespace is condensed into a single space
node.content = ' '
}
} else if (shouldCondense) {
// in condense mode, consecutive whitespaces in text are condensed
// down to a single space.
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
}
}
// Remove comment nodes if desired by configuration.
else if (node.type === NodeTypes.COMMENT && !context.options.comments) {
// 非空文件
removedWhitespace = true
nodes[i] = null as any
}
}
if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
// remove leading newline per html spec
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
const first = nodes[0]
if (first && first.type === NodeTypes.TEXT) {
// 根据HTML规范删除前导换行符
first.content = first.content.replace(/^\r?\n/, '')
}
}
}
return removedWhitespace ? nodes.filter(Boolean) : nodes
}
其中有很多判断,我们只看四种情况解析:注释节点、插值、普通文本、元素节点:
1.注释节点解析:
function parseComment(context: ParserContext): CommentNode {
__TEST__ && assert(startsWith(context.source, '<!--'))
const start = getCursor(context)
let content: string
// Regular comment.
const match = /--(\!)?>/.exec(context.source)
if (!match) {
// 没有匹配的注释结束符
content = context.source.slice(4)
advanceBy(context, context.source.length)
emitError(context, ErrorCodes.EOF_IN_COMMENT)
} else {
if (match.index <= 3) {
// 非法的注释符号
emitError(context, ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT)
}
if (match[1]) {
// 注释结束符不正确
emitError(context, ErrorCodes.INCORRECTLY_CLOSED_COMMENT)
}
// 获取注释的内容
content = context.source.slice(4, match.index)
// Advancing with reporting nested comments.
// 截取到注释结尾之间的代码,用于后续判断嵌套注释
const s = context.source.slice(0, match.index)
let prevIndex = 1,
nestedIndex = 0
// 判断嵌套注释符的情况,存在即报错
while ((nestedIndex = s.indexOf('<!--', prevIndex)) !== -1) {
advanceBy(context, nestedIndex - prevIndex + 1)
if (nestedIndex + 4 < s.length) {
emitError(context, ErrorCodes.NESTED_COMMENT)
}
prevIndex = nestedIndex + 1
}
// 前进代码到注释结束符后
advanceBy(context, match.index + match[0].length - prevIndex + 1)
}
return {
// 表示是注释节点
type: NodeTypes.COMMENT,
// 注释的内容
content,
// 注释代码的开头和结束位置信息
loc: getSelection(context, start)
}
}
可以发现就是利用正则去匹配,截取注释内容,其他情况报错,最后调用了advanceBy去前进代码,更新content解析上下文:
function advanceBy(context: ParserContext, numberOfCharacters: number): void {
const { source } = context
__TEST__ && assert(numberOfCharacters <= source.length)
// 更新context的offset、line、column
advancePositionWithMutation(context, source, numberOfCharacters)
// 更新context的source
context.source = source.slice(numberOfCharacters)
}
// packages/compiler-core/src/utils.ts
export function advancePositionWithMutation(
pos: Position,
source: string,
numberOfCharacters: number = source.length
): Position {
let linesCount = 0
let lastNewLinePos = -1
for (let i = 0; i < numberOfCharacters; i++) {
if (source.charCodeAt(i) === 10 /* newline char code */) {
linesCount++
lastNewLinePos = i
}
}
pos.offset += numberOfCharacters
pos.line += linesCount
pos.column =
lastNewLinePos === -1
? pos.column + numberOfCharacters
: numberOfCharacters - lastNewLinePos
return pos
}
这时注释部分代码就处理完了,接着看插值的解析:
2.**插值解析: ** {{ msg }}
function parseInterpolation(
context: ParserContext,
mode: TextModes
): InterpolationNode | undefined {
// 从默认配置中获取插值开始和结束分隔符,默认是 {{ 和 }}
const [open, close] = context.options.delimiters
__TEST__ && assert(startsWith(context.source, open))
// 跳过开始分隔符的位置,查找结束分割符
const closeIndex = context.source.indexOf(close, open.length)
if (closeIndex === -1) {
// 不存在就报错
emitError(context, ErrorCodes.X_MISSING_INTERPOLATION_END)
return undefined
}
const start = getCursor(context)
// 代码前进到插值开始分隔符后
advanceBy(context, open.length)
// 内部插值开始位置
const innerStart = getCursor(context)
// 内部插值结束位置
const innerEnd = getCursor(context)
// 插值原始内容长度
const rawContentLength = closeIndex - open.length
// 插值原始内容
const rawContent = context.source.slice(0, rawContentLength)
// 获取插值的内容,并前进代码到插值内容后
const preTrimContent = parseTextData(context, rawContentLength, mode)
const content = preTrimContent.trim()
// 内容相对于插值开始分隔符的头偏移
const startOffset = preTrimContent.indexOf(content)
if (startOffset > 0) {
// 更新内部插值开始位置
advancePositionWithMutation(innerStart, rawContent, startOffset)
}
// 内容相对于插值结束分隔符的尾偏移
const endOffset =
rawContentLength - (preTrimContent.length - content.length - startOffset)
// 更新内部插值结束位置
advancePositionWithMutation(innerEnd, rawContent, endOffset)
// 前进代码到插值结束分隔符后
advanceBy(context, close.length)
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
// Set `isConstant` to false by default and will decide in transformExpression
constType: ConstantTypes.NOT_CONSTANT,
content,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
}
}
3.普通文本解析:This is my app
function parseText(context: ParserContext, mode: TextModes): TextNode {
__TEST__ && assert(context.source.length > 0)
// 文本结束符:对于一段文本来说,都是在遇到 < 或者插值分隔符 {{ 结束
const endTokens =
mode === TextModes.CDATA ? [']]>'] : ['<', context.options.delimiters[0]]
let endIndex = context.source.length
// 遍历文本结束符,匹配找到结束的位置
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i], 1)
if (index !== -1 && endIndex > index) {
endIndex = index
}
}
__TEST__ && assert(endIndex > 0)
const start = getCursor(context)
// 获取文本的内容,并前进代码到文本的内容后
const content = parseTextData(context, endIndex, mode)
return {
type: NodeTypes.TEXT,
content,
loc: getSelection(context, start)
}
}
function parseTextData(
context: ParserContext,
length: number,
mode: TextModes
): string {
const rawText = context.source.slice(0, length)
advanceBy(context, length)
if (
mode === TextModes.RAWTEXT ||
mode === TextModes.CDATA ||
!rawText.includes('&')
) {
return rawText
} else {
// DATA or RCDATA containing "&"". Entity decoding required.
return context.options.decodeEntities(
rawText,
mode === TextModes.ATTRIBUTE_VALUE
)
}
}
4.元素节点解析:
<div class="app">
<hello :msg="msg"></hello>
</div>
当前代码以 < 开头,并且后面跟着字母:
function parseElement(
context: ParserContext,
ancestors: ElementNode[]
): ElementNode | undefined {
__TEST__ && assert(/^<[a-z]/i.test(context.source))
// Start tag.
// 是否在 pre 标签内
const wasInPre = context.inPre
// 是否在 v-pre 指令内
const wasInVPre = context.inVPre
// 获取当前元素的父标签节点
const parent = last(ancestors)
// 解析开始标签,生成一个标签节点,并前进代码到开始标签后
const element = parseTag(context, TagType.Start, parent)
// 是否 在 pre 标签的边界
const isPreBoundary = context.inPre && !wasInPre
// 是否在 v-pre 指令的边界
const isVPreBoundary = context.inVPre && !wasInVPre
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
// 如果是自闭合标签,直接返回标签节点
// #4030 self-closing <pre> tag
if (isPreBoundary) {
context.inPre = false
}
if (isVPreBoundary) {
context.inVPre = false
}
return element
}
// Children.
// 下边是处理子节点
// 先把标签节点添加到ancestors,进行入栈
ancestors.push(element)
const mode = context.options.getTextMode(element, parent)
// 递归解析子节点,传入ancestors
const children = parseChildren(context, mode, ancestors)
// ancestors出栈
ancestors.pop()
// 添加到children属性中
element.children = children
// End tag.
// 结束标签
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)
if (isPreBoundary) {
context.inPre = false
}
if (isVPreBoundary) {
context.inVPre = false
}
return element
}
元素节点解析主要做了三件事:解析开始标签、解析子节点、解析闭合标签,首先来看开始标签的解析,是通过parseTag解析并创建一个标签节点:
// parseTag 最终返回的值就是一个描述标签节点的对象,其中 type 表示它是一个标签节点,tag 表示标签名,tagType 表示标签的类型,
// content 表示文本的内容,isSelfClosing 表示是否是一个闭合标签,loc 表示文本的代码开头和结束的位置信息,children 是标签的
// 子节点数组,会先初始化为空。
function parseTag(
context: ParserContext,
type: TagType,
parent: ElementNode | undefined
): ElementNode | undefined {
// Tag open.
// 标签打开
const start = getCursor(context)
// 匹配文本结束的位置,看上面图片匹配结果
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
// 标签名
const tag = match[1]
const ns = context.options.getNamespace(tag, parent)
// 前进代码到标签文本结束的位置
advanceBy(context, match[0].length)
// 前进代码到标签文本后面的空白字符后
advanceSpaces(context)
// save current state in case we need to re-parse attributes with v-pre
// 保存当前状态以防后面需要用v-pre重新解析属性
const cursor = getCursor(context)
const currentSource = context.source
// check <pre> tag
// 检查是不是一个 pre 标签
if (context.options.isPreTag(tag)) {
context.inPre = true
}
// Attributes.
// 解析标签中的属性, 并前进代码到属性后
let props = parseAttributes(context, type)
// check v-pre
// 检查属性中有没有 v-pre 指令
if (
type === TagType.Start &&
!context.inVPre &&
props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
) {
context.inVPre = true
// reset context
// 重置context
extend(context, cursor)
context.source = currentSource
// re-parse attrs and filter out v-pre itself
// 重新解析属性,并把 v-pre 过滤了
props = parseAttributes(context, type).filter(p => p.name !== 'v-pre')
}
// Tag close.
// 闭合标签
let isSelfClosing = false
if (context.source.length === 0) {
emitError(context, ErrorCodes.EOF_IN_TAG)
} else {
// 判断是否自闭合标签
isSelfClosing = startsWith(context.source, '/>')
if (type === TagType.End && isSelfClosing) {
// 闭合标签不应该自闭合标签,报错
emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
}
// 前进代码到闭合标签后
advanceBy(context, isSelfClosing ? 2 : 1)
}
if (type === TagType.End) {
return
}
// 判断标签类型。是组件、插槽还是模板
let tagType = ElementTypes.ELEMENT
if (!context.inVPre) {
if (tag === 'slot') {
tagType = ElementTypes.SLOT
} else if (tag === 'template') {
if (
props.some(
p =>
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
)
) {
// 判断是否有is属性
tagType = ElementTypes.TEMPLATE
}
} else if (isComponent(tag, props, context)) {
tagType = ElementTypes.COMPONENT
}
}
return {
type: NodeTypes.ELEMENT,
ns,
tag,
tagType,
props,
isSelfClosing,
children: [],
loc: getSelection(context, start),
codegenNode: undefined // to be created during transform phase
}
}
子节点解析完成后,baseParse就剩使用createRoot创建AST根节点,来看createRoot实现:
// packages/compiler-core/src/ast.ts
export function createRoot(
children: TemplateChildNode[],
loc = locStub
): RootNode {
// 返回一个js对象,作为AST根节点
return {
// 根节点类型
type: NodeTypes.ROOT,
// 之前解析的子节点数组
children,
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc
}
}