面试题:
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
中包含多个节点,开发环境报出警告。