vue中的template为啥只能有一个根节点(源码分析)

119 阅读3分钟

面试题:vuetemplate模板中,为啥只能有一个根节点?

答案:因为,vue生成render函数需要ast树,构建ast树需要有一个根节点,就像自然界中的树,它只有一个根一样。

// main.js中
new Vue({
  el: "#app",
  template: `<h1>title</h1><p>phrase</p>`,
});

以上例子,页面只显示title,控制台报compiling的报错

image.png

那么,接下来就一点一点分析报错的原因。

解析思路ast树是由const ast = parse(template.trim(), options)来获取,其中定义了栈const stack = []。在解析的过程中,将template作为第一个参数,startendcharscomment等主要方法作为第二个对象参数中的方法,执行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

image.png

1、遇到<h1>开始标签

遇到<h1>开始标签后,先移动指针位置:

image.png

接着执行start函数,此时会执行到逻辑:root = element,即root为精简后的{tag:h1}。再执行逻辑currentParent = element,即currentParent为精简后的{tag:h1}。又会执行stack.push(element),此时栈中就有精简后的{tag:h1}

image.png

2、遇到</h1>闭合标签

遇到</h1>闭合标签后,先移动指针位置:

image.png

接着执行end函数,通过var element = stack[stack.length - 1]获取到element的值为栈顶元素,即为精简后的{tag:h1}。再通过stack.length -= 1的方式弹出栈顶元素,此时通过currentParent = stack[stack.length - 1]获取到的当前currentParentundefined

image.png

end函数最后回执行closeElement,在h1闭合的情况下,element就是root,所以第一个if逻辑不执行,currentParentundefined不存在,所以第二个if逻辑不执行。

3、遇到<p>开始标签

遇到<p>开始标签后,先移动指针位置:

image.png

接着执行start函数,执行currentParent = elementcurrentParent为精简后的{tag:p}。又会执行stack.push(element),此时栈中就有精简后的{tag:p}

image.png

4、遇到</p>闭合标签

遇到</p>闭合标签后,先移动指针位置:

image.png

接着执行end函数,通过var element = stack[stack.length - 1]获取到element的值为栈顶元素,即为精简后的{tag:p}。再通过stack.length -= 1的方式弹出栈顶元素,此时通过currentParent = stack[stack.length - 1]获取到的当前currentParentundefined

image.png

end函数最后回执行closeElement,在p闭合的情况下,element是精简后的{tag:p},而root是首次遇到h1标签时的精简后的{tag:h1},同时栈为空,满足!stack.length && element !== roottrue,所以会执行到第一个if逻辑。当前例子中两个节点都不是ifelse if逻辑,那么在开发环境,就会报文中的错误:组件template应该只包含一个根节点,如果在多个元素上使用v-if,则使用v-else-if来连接它们。

总结

在解析模板的过程中,会有个指针去不断的扫描模板template,当遇到开始标签时会执行start函数,如果遇到闭合标签时会执行end函数,并且执行closeElement去管理ast树。管理树的过程中如果遇到栈为空,并且elementroot不相等的情况,则认为当前组件template中包含多个节点,开发环境报出警告。