面试题:
vue的template模板中,为啥只能有一个根节点?
答案:因为,vue生成render函数需要ast树,构建ast树需要有一个根节点,就像自然界中的树,它只有一个根一样。
// main.js中
new Vue({
el: "#app",
template: `<h1>title</h1><p>phrase</p>`,
});
以上例子,页面只显示title,控制台报compiling的报错
那么,接下来就一点一点分析报错的原因。
解析思路:ast树是由const ast = parse(template.trim(), options)来获取,其中定义了栈const stack = []。在解析的过程中,将template作为第一个参数,start、end、chars和comment等主要方法作为第二个对象参数中的方法,执行parseHTML方法的过程中会不断的往stack中推入或弹出astElement,以便于通过栈结构来生成一个ast树。
- 精简后的
start函数
start (tag, attrs, unary, start, end) {
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (!root) {
root = element
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
},
- 精简后的
end函数
end (tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
},
- 精简后的
closeElement函数
function closeElement (element) {
// tree management
if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(element)
}
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production') {
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`,
{ start: element.start }
)
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
}
currentParent.children.push(element)
element.parent = currentParent
}
}
}
parseHTML相当于处理<h1>title</h1><p>phrase</p>字符串的一个指针,遇到开始标签执行start,遇到闭合标签执行end。
1、遇到<h1>开始标签
遇到<h1>开始标签后,先移动指针位置:
接着执行start函数,此时会执行到逻辑:root = element,即root为精简后的{tag:h1}。再执行逻辑currentParent = element,即currentParent为精简后的{tag:h1}。又会执行stack.push(element),此时栈中就有精简后的{tag:h1}:
2、遇到</h1>闭合标签
遇到</h1>闭合标签后,先移动指针位置:
接着执行end函数,通过var element = stack[stack.length - 1]获取到element的值为栈顶元素,即为精简后的{tag:h1}。再通过stack.length -= 1的方式弹出栈顶元素,此时通过currentParent = stack[stack.length - 1]获取到的当前currentParent为undefined。
end函数最后回执行closeElement,在h1闭合的情况下,element就是root,所以第一个if逻辑不执行,currentParent为undefined不存在,所以第二个if逻辑不执行。
3、遇到<p>开始标签
遇到<p>开始标签后,先移动指针位置:
接着执行start函数,执行currentParent = element后currentParent为精简后的{tag:p}。又会执行stack.push(element),此时栈中就有精简后的{tag:p}:
4、遇到</p>闭合标签
遇到</p>闭合标签后,先移动指针位置:
接着执行end函数,通过var element = stack[stack.length - 1]获取到element的值为栈顶元素,即为精简后的{tag:p}。再通过stack.length -= 1的方式弹出栈顶元素,此时通过currentParent = stack[stack.length - 1]获取到的当前currentParent为undefined。
end函数最后回执行closeElement,在p闭合的情况下,element是精简后的{tag:p},而root是首次遇到h1标签时的精简后的{tag:h1},同时栈为空,满足!stack.length && element !== root为true,所以会执行到第一个if逻辑。当前例子中两个节点都不是if或else if逻辑,那么在开发环境,就会报文中的错误:组件template应该只包含一个根节点,如果在多个元素上使用v-if,则使用v-else-if来连接它们。
总结
在解析模板的过程中,会有个指针去不断的扫描模板
template,当遇到开始标签时会执行start函数,如果遇到闭合标签时会执行end函数,并且执行closeElement去管理ast树。管理树的过程中如果遇到栈为空,并且element和root不相等的情况,则认为当前组件template中包含多个节点,开发环境报出警告。