Vue 3.0 模板编译过程
模板编译过程
当组件对象上不存在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.HTMLconst nodes: TemplateChildNode[] = []// 遍历模板字符串// 调用isEnd判断是否已经遍历结束// 如果模板为空,或者匹配到结束标签”</“开头并且结束标签的tag和ancestors中的相同// 则跳出循环处理结束标签while (!isEnd(context, mode, ancestors)) {__TEST__ && assert(context.source.length > 0)// 获得当前剩余的模板const s = context.sourcelet node: TemplateChildNode | TemplateChildNode[] | undefined = undefinedif (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 = falseif (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 = truenodes[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 = truenodes[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.inPreconst 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 && !wasInPreconst 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重新设置为falseif (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))// 获得标签内的tagconst 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指令,需要重新解析propsconst cursor = getCursor(context)const currentSource = context.source// 解析propslet props = parseAttributes(context, type)// 如果是pre标签 将上下文的inPre设置为trueif (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 contextextend(context, cursor)context.source = currentSource// 解析后过滤v-pre指令props = parseAttributes(context, type).filter(p => p.name !== 'v-pre')}// Tag close.// 闭合开始或者结束标签let isSelfClosing = falseif (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.ELEMENTconst 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设置为COMPONENTif (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT} else if (// 如果is存在或者标签是Teleport、Suspense、KeepAlive、BaseTransition// 中的一个、或者标签是内建的Transition、TransitionGroup、或者标签以大写开头、// 或者标签等于'component',都将tagType设置为COMPONENThasVIs ||isCoreComponent(tag) ||(options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||/^[A-Z]/.test(tag) ||tag === 'component') {tagType = ElementTypes.COMPONENT}// 如果tag为slot、则tagType = ElementTypes.SLOTif (tag === 'slot') {tagType = ElementTypes.SLOT} else if (// 如果tag为template、并且存在`if,else,else-if,for,slot`等指令,则tagType = ElementTypes.TEMPLATEtag === '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 } = contextconst { children } = rootconst child = children[0]// 当ast树的根节点的子节点只有一个节点if (children.length === 1) {// 并且它是一个element节点if (isSingleElementRoot(root, child) && child.codegenNode) {// 则将该节点标记为block节点const codegenNode = child.codegenNodeif (codegenNode.type === NodeTypes.VNODE_CALL) {codegenNode.isBlock = truehelper(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)breakcase NodeTypes.TEXT:genText(node, context)breakcase NodeTypes.COMMENT:genComment(node, context)breakcase NodeTypes.VNODE_CALL:genVNodeCall(node, context)break...}}