# 模板编译过程
当组件对象上不存在render函数时,Vue根据组件上的template,调用compile方法来编译模板, 并生成渲染函数
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters
})
模板的编译过程主要分为三步:
- 1、ast parse: 根据传入的template生成ast树
- 2、ast transform: 优化ast节点,生成codegenNode
- 3、codegen: 根据codegenNode拼接渲染函数
# 生成ast
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)
)
}
function parseChildren(
context: ParserContext,
mode: TextModes,
ancestors: ElementNode[]
): TemplateChildNode[] {
const parent = last(ancestors)
const ns = parent ? parent.ns : Namespaces.HTML
const nodes: TemplateChildNode[] = []
// 遍历模板字符串
// 调用isEnd判断是否已经遍历结束
// 如果模板为空,或者匹配到结束标签”</“开头并且结束标签的tag和ancestors中的相同
// 则跳出循环处理结束标签
while (!isEnd(context, mode, ancestors)) {
__TEST__ && assert(context.source.length > 0)
// 获得当前剩余的模板
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.length === 1) {
// 模板长度只有1时,报错
emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
} else if (s[1] === '!') {
// 当模板以"<!"开头,分为以下几种
if (startsWith(s, '<!--')) {
// 匹配到"<!--" 调用parseComment生成注释节点
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) {
// 匹配到"<!--DOCTYPE" 调用parseBogusComment生成注释节点
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] === '/') {
// 以“</”开头
if (s.length === 2) {
// 如果模板只剩“</"2个字符,报错
emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 2)
} else if (s[2] === '>') {
// 如果匹配到”</>“,报错并调用advanceBy跳过这3个字符
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])) {
// 以”< + 字母开头“则调用parseElement生成element节点
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)
}
}
}
// 如果node不存在,即不符合上面任何一种,则调用parseText找到文本第一个中"<"或者"{{"
// 将之前的文本生成一个文本节点
if (!node) {
node = parseText(context, mode)
}
// 将生成的节点保存到nodes中
// 如果当前生成的节点是文本节点,并且前一个节点也是文本节点
// 则合并两个文本节点
if (isArray(node)) {
for (let i = 0; i < node.length; i++) {
pushNode(nodes, node[i])
}
} else {
pushNode(nodes, node)
}
}
// 处理节点间的空格
let removedWhitespace = false
if (mode !== TextModes.RAWTEXT) {
// 如果不在pre节点中
if (!context.inPre) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
// 遍历所有节点,如果是文本节点
if (node.type === NodeTypes.TEXT) {
// 如果文本开头不存在换行
// 则以下情况需要过滤空白节点
if (!/[^\t\r\n\f ]/.test(node.content)) {
const prev = nodes[i - 1]
const next = nodes[i + 1]
// 1、空白节点是第一或者最后一个节点
// 2、空白节点在注释节点的前或者后
// 3、空白节点在2个element节点中间,并且存在换行
if (
!prev ||
!next ||
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 {
// 否则,将文本内的连续空格压缩为单个空格
node.content = ' '
}
} else {
// 否则将换行替换为空格
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
}
} else if (
!__DEV__ &&
node.type === NodeTypes.COMMENT &&
!context.options.comments
) {
// 生产环境中过滤所有注释节点
removedWhitespace = true
nodes[i] = null as any
}
}
} else if (parent && context.options.isPreTag(parent.tag)) {
// 如果当前节点的父节点是pre,则去掉当前文本开头的换行
const first = nodes[0]
if (first && first.type === NodeTypes.TEXT) {
first.content = first.content.replace(/^\r?\n/, '')
}
}
}
// 如果需要过滤空白节点则调用filter过滤
return removedWhitespace ? nodes.filter(Boolean) : nodes
}
可以看到parseChildren方法大部分判断都是处理一些特殊的节点,只有符合”< + 字母的情况“才调用parseElement 去创建一个ast节点
function parseElement(
context: ParserContext,
ancestors: ElementNode[]
): ElementNode | undefined {
__TEST__ && assert(/^<[a-z]/i.test(context.source))
// 节点是否在pre标签或者v-pre指令中
const wasInPre = context.inPre
const wasInVPre = context.inVPre
// 获取父节点
const parent = last(ancestors)
// 调用parseTag创建ast节点
const element = parseTag(context, TagType.Start, parent)
// 如果解析后节点是pre或者有v-pre指令,则说明该节点是pre或者存在v-pre指令
const isPreBoundary = context.inPre && !wasInPre
const isVPreBoundary = context.inVPre && !wasInVPre
// 如果是自闭合的标签,则直接返回
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element
}
// 处理子节点
// 将当前节点添加到ancestors中
ancestors.push(element)
const mode = context.options.getTextMode(element, parent)
// 递归调用parseChildren处理子节点
const children = parseChildren(context, mode, ancestors)
ancestors.pop()
// 将子节点添加到children属性上
element.children = children
// 处理闭合标签
// 当匹配到”</",并且闭合标签内的tag和起始标签的tag相同
// 则调用parseTag闭合节点
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)
}
}
}
// 调用getSelection获得节点在模板中的起始、结束位置以及节点的原字符串
element.loc = getSelection(context, element.loc.start)
// 如果当前节点是pre或者存在v-pre指令,则解析完当前节点后需要将inPre、inVPre重新设置为false
if (isPreBoundary) {
context.inPre = false
}
if (isVPreBoundary) {
context.inVPre = false
}
return element
}
从parseElement中可以看到不管是开始还是结束标签,都需要调用parseTag来处理,根据传入的type来判断是处理开始还是结束标签
function parseTag(
context: ParserContext,
type: TagType,
parent: ElementNode | undefined
): ElementNode {
__TEST__ && assert(/^<\/?[a-z]/i.test(context.source))
__TEST__ &&
assert(
type === (startsWith(context.source, '</') ? TagType.End : TagType.Start)
)
// 获得标签内的tag
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)
// 将模板前进到第一个props之前
advanceBy(context, match[0].length)
advanceSpaces(context)
// save current state in case we need to re-parse attributes with v-pre
// 保存解析props之前的模板,如果解析props后存在v-pre指令,需要重新解析props
const cursor = getCursor(context)
const currentSource = context.source
// 解析props
let props = parseAttributes(context, type)
// 如果是pre标签 将上下文的inPre设置为true
if (context.options.isPreTag(tag)) {
context.inPre = true
}
// 如果存在v-pre指令
if (
!context.inVPre &&
props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
) {
// 则将inVPre设置为true,并将解析props之前的模板赋值,并重新解析
context.inVPre = true
// reset context
extend(context, cursor)
context.source = currentSource
// 解析后过滤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, '/>')
// 如果是在处理结束标签时,说明匹配到”</tag/>“,则直接报错
if (type === TagType.End && isSelfClosing) {
emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
}
// 根据是否是自闭合标签前进">"或者"/>"
advanceBy(context, isSelfClosing ? 2 : 1)
}
// 使用tagType细分标签的类型
let tagType = ElementTypes.ELEMENT
const options = context.options
// 如果当前标签不是在v-pre中,也不是自定义标签
if (!context.inVPre && !options.isCustomElement(tag)) {
// 那么先判断有没有is指令
const hasVIs = props.some(
p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
)
if (options.isNativeTag && !hasVIs) {
// 如果不存在is指令,并且不是原生标签,则tagType设置为COMPONENT
if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
} else if (
// 如果is存在或者标签是Teleport、Suspense、KeepAlive、BaseTransition
// 中的一个、或者标签是内建的Transition、TransitionGroup、或者标签以大写开头、
// 或者标签等于'component',都将tagType设置为COMPONENT
hasVIs ||
isCoreComponent(tag) ||
(options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
/^[A-Z]/.test(tag) ||
tag === 'component'
) {
tagType = ElementTypes.COMPONENT
}
// 如果tag为slot、则tagType = ElementTypes.SLOT
if (tag === 'slot') {
tagType = ElementTypes.SLOT
} else if (
// 如果tag为template、并且存在`if,else,else-if,for,slot`等指令,则tagType = ElementTypes.TEMPLATE
tag === 'template' &&
props.some(p => {
return (
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
)
})
) {
tagType = ElementTypes.TEMPLATE
}
}
// 返回ast对象
return {
type: NodeTypes.ELEMENT,
ns,
tag,
tagType,
props,
isSelfClosing,
children: [],
loc: getSelection(context, start),
codegenNode: undefined // to be created during transform phase
}
}
通过模板编译生成ast节点的过程其实就是不断遍历模板去匹配标签,并且通过递归编译子节点的过程建立节点的父子关系。
# transform
解析模板生成的ast树并不能直接用于生成渲染函数,而是要经过transform方法遍历所有ast节点,根据 ast节点的类型来调用不同helper函数,这些helper函数将会对针对特定的ast节点进一步优化,生成可供codegen函数使用的codegenNode,这里就不具体对每一个helper函数展开分析,作为一个例子我们仅分析一下根ast节点的transform过程:
之前在分析patch过程的时候我们说到,Vue3.0现在已经支持在模板写多个根节点了,但是一棵树总是需要一个根节点的,所以Vue会帮你生成一个FRAGMENT节点作为根节点,这个节点不会在真实的DOM中显示出来,这个过程就是在ast树的根节点的transform过程中实现的。
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
const child = children[0]
// 当ast树的根节点的子节点只有一个节点
if (children.length === 1) {
// 并且它是一个element节点
if (isSingleElementRoot(root, child) && child.codegenNode) {
// 则将该节点标记为block节点
const codegenNode = child.codegenNode
if (codegenNode.type === NodeTypes.VNODE_CALL) {
codegenNode.isBlock = true
helper(OPEN_BLOCK)
helper(CREATE_BLOCK)
}
root.codegenNode = codegenNode
} else {
// - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched.
// root codegen falls through via genNode()
root.codegenNode = child
}
} else if (children.length > 1) {
// 如果子节点存在多个,则生成一个FRAGMENT节点保存在codegenNode属性上
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`,
undefined,
undefined,
true
)
} else {
// no children = noop. codegen will return null.
}
}
可以看到transform的过程就是在ast节点的基础上通过特定的helper函数,这些函数会修改原本ast节点上的属性,并将修改后的节点保存在codegenNode属性上,用于生成渲染函数
# codegen
codegen的过程其实就是拼接字符串,这里截取了genNode函数的部分代码,可以看到通过判断codegenNode上的类型调用不同的函数,这些函数的实现基本上就是根据codegenNode上的内容输出相应的字符串然后拼接到最终的代码字符串上,而整个codegen的过程就是递归的调用genNode函数遍历所有codegenNode,最后再通过new Function来创建渲染函数。
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
...
switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
__DEV__ &&
assert(
node.codegenNode != null,
`Codegen node is missing for element/if/for node. ` +
`Apply appropriate transforms first.`
)
genNode(node.codegenNode!, context)
break
case NodeTypes.TEXT:
genText(node, context)
break
case NodeTypes.COMMENT:
genComment(node, context)
break
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context)
break
...
}
}
← watchApi