简介前段时间看到这样一个关于Vue的问题,为什么每个组件模板只有一个根?可能在平时的开发中,大家经常会使用模板来写html。当然,不排除使用JSX和render()函数。但是,在它们的核心,它们都以render()函数结束。然后,通过render()函数将其转化为VirtualDOM(以下统称为VNode)。将render()函数转换为VNode的过程是由createElement()函数完成的。因此,本文将首先描述为什么Vue限制模板有且只能有一个根。然后,分析Vue是如何避免多根的。那么,我们就从源码的角度来深究一下这个过程吧!1.为什么模板被限制为只有一个根这里我们从两个方面来解释,一个是createElement()的执行过程和定义,一个是VNode的定义。1.1createElement()createElement()函数在源码中被设计为render()函数的一个参数。所以官方文档也解释了如何使用render()函数来创建组件。并且createElement()将在_render阶段执行:...const{render,_parentVnode}=vm.$options...vnode=render.call(vm._renderProxy,vm.$createElement);可以看出很简单,源码通过call()将当前实例作为context上下文,将$createElement作为参数传入。Vue2x源码中大量使用了call和apply。例如,经典的$set()API用于实现数组变化的响应式处理。有兴趣的可以看看。$createElement的定义又是这样的:vm.$createElement=(a,b,c,d)=>createElement(vm,a,b,c,d,true)需要注意的是,这是我们写的时候render()是手工调用的,如果写了模板,会调用另一个vm._c方法。两者的区别在于createElement()的最后一个参数为true,后者为false。而这里,这个createElement()实质上是调用了_createElement()方法,它的定义::VNodeData,//vnodedatachildren?:any,normalizationType?:number):VNode|Array{...}现在,我们看到了我们通常使用的createElement()的真面目。这里,我们先不看函数内部的执行逻辑。这里我们分析一下这五个参数:context,就是vue在_render阶段传入的当前实例标签,就是我们使用createElement时定义的根节点HTML标签名数据,就是我们使用createElement传入节点的属性,比如class,style,props等children,我们使用createElement传入节点包含的子节点,通常是一个normalizationType的数组,用于判断子节点的平级对于数组,是否使用simple迭代或递归处理,前者针对简单的二维,后者针对多维。可以看出createElement()的设计是针对一个节点,然后创建一个带有子组件的VNode。而且它不给你留下创建多个root的机会,你只能传一个root的tag,其他都是它的选项。1.2在VNode之前,我们分析了createElement()的调用时机,知道它最终会返回VNode。那么,现在我们来看VNode的定义:exportdefaultclassVNode{tag:string|空白;数据:VNodeData|空白;孩子们:?数组;文本:字符串|空白;榆树:节点|空白;ns:字符串|空白;上下文:组件|空白;//在此组件的作用域中呈现key:string|编号|空白;组件选项:VNodeComponentOptions|空白;componentInstance:组件|空白;//组件实例父级:VNode|空白;//组件占位符节点//严格内部raw:boolean;//包含原始HTML?(仅限服务器)isStatic:boolean;//提升的静态节点isRootInsert:boolean;//进入转换检查所必需的isComment:boolean;//空注释占位符?已克隆:布尔值;//是克隆节点吗?isOnce:布尔值;//是一个v-once节点吗?异步工厂:函数|空白;//异步组件工厂函数asyncMeta:Object|空白;isAsyncPlaceholder:布尔值;ssrContext:对象|空白;fnContext:组件|空白;//真实上下文vm对于功能节点fnOptions:?ComponentOptions;//用于SSR缓存devtoolsMeta:?Object;//用于存储devtools的功能渲染上下文fnScopeId:?string;//功能范围id支持构造函数(tag?:string,data?:VNodeData,children?:?Array,text?:string,elm?:Node,context?:Component,componentOptions?:VNodeComponentOptions,asyncFactory?:Function){...}...}可以看到VNode的属性还是挺多的。这次我们只看VNode的前三个属性:tag,也就是VNode对于data的标签名,也就是VNodechildren的一些属性,也就是VNode的子节点,是一个VNode数组。ObviousVNode的设计也是一个root,然后children继续extend它。这就呼应了之前createElement()的设计,不可能有多个根。1.3小结可以看出VNode和createElement()的设计只是针对单根的情况,最终形成树状结构。那么我想这时候可能有人会问为什么要设计成树形结构呢?.要解决这个问题,有两个方面。一方面,树形结构的VNode转化为真实DOM后,我们只需要将根VNode的真实DOM挂载到页面即可。另一方面,DOM本身是一个树结构,所以VNode也被设计成一个树结构,后面我们会提到模板编译阶段的AST抽象语法树,也是一个树结构。因此,统一结构可以实现非常方便的类型转换,即从AST到Render函数,从Render函数到VNode,最后从VNode到真正的DOM。并且,大家可以想一个场景,如果有多个根,当你把VNode转换成真正的DOM,挂载到页面的时候,是不是要遍历DOMCollection,然后挂载,这个阶段就是操作DOM阶段。每个人都知道的一件事是操作DOM是昂贵的。所以,一根根的好处体现在这个时候它的好处。其实这个过程让我想起了小红书在讲文档分片的时候,提倡先将要创建的DOM添加到文档分片中,再将文档分片添加到页面中。2.如何避免出现多根的情况2.1模板编译过程在我们平时的开发中,通常是在.vue文件中写,然后在中创建一个div作为根,然后在根中写入描述这个.vue文件的html标签。当然你也可以直接写render()函数。文章开头我们也说过,无论是写模板还是在Vue中渲染,最终都会变成render()函数。在平时的开发中,我们更多的是使用模板。所以这个过程需要Vue编译模板。编译模板的过程将如下:根据模板生成AST(抽象语法树)优化AST,即判断AST节点的静态节点或静态根节点,以便patch判断根据Vue中的AST的可执行函数。这个阶段定义了_c、_l等很多函数。本质上,它们是对render()函数的封装。源码中定义了这三个步骤:exportconstcreateCompiler=createCompilerCreator(functionbaseCompile(template:string,options:CompilerOptions):CompiledResult{//生成ASTconstast=parse(template.trim(),options)if(options.optimize!==false){//优化ASToptimize(ast,options)}//生成可以执行的函数constcode=generate(ast,options)return{ast,render:code.render,staticRenderFns:code.staticRenderFns}})需要注意的是,Vue-CLI提供了两个版本,Runtime-Compiler和Runtime,两者的区别前者是前者可以将模板编译成render()函数,而后者必须写render()手动操作。对于开发,如果你写了多个根组件,解析时会生成AST抽象语法创建树时,Vue会过滤掉多余的根,只识别第一个根。parse的整个过程其实就是正则匹配的过程,而这个过程会用栈来存放起始标签。整个parse过程流程图:那么我们通过一个例子来分析一下,这个例子是处理多根的。假设我们此时定义了这样一个模板:
显然,它是多根的。当处理第一个时,会创建对应的ASTElement,其结构如下:{type:1,tag:"div",attrsList:[],attrsMap:{},rawAttrsMap:{},parent:undefined,children:[],start:0,end:5}此时会将这个ASTElement加入栈中,然后删除原字符串中的
,将root设置为这个ASTElement。然后,继续遍历。对于,也会创建一个ASTElement,压栈,然后删除,下次继续。接下来会匹配,此时会处理标签的结尾,比如会匹配栈顶的ASTElement的标签,然后出栈。接下来匹配到
,和span一样的操作。最后,对于第二个根
,执行与上面相同的操作。但是在处理
时,会进入判断多根的逻辑,即此时字符串已经处理完毕,但是这个结束标签对应的ASTElement不等于我们原来定义的根。所以这时候会报错:Componenttemplateshouldcontainexactlyonerootelement。如果您在多个元素上使用v-if,请改用v-else-if链接它们。而且ASTElement不会被添加到最终的AST中,所以将来不可能有多个根。同时这个错误也提醒我们,如果要使用多根,需要使用if条件来判断。可见,模板编译解析阶段的最终目标是构建AST抽象语法树。因此,它会在创建第一个ASTElement时确定AST的根,从而保证根的唯一性。2.2_render过程不了解vue初始化过程的同学可能对_render过程不是很清楚。你可以理解为渲染过程。这个阶段会调用render方法生成一个VNode,对VNode进行一些处理,最后返回一个VNode。相对于模板编译过程,_render过程的判断相对简单:('从渲染函数返回多个根节点。渲染函数'+'应该返回一个根节点。',vm);}vnode=createEmptyVNode();}我们在讲createElement的时候也提到过render()需要返回VNode。所以,这里为了防止偏操作,返回一个包含多个VNodes的数组。结语看完想必大家明白为什么Vue中模板只有一个root了。Vue的设计出发点可能很简单,就是为了减少挂载时对DOM的操作。但是它是如何处理多根情况的,以及相关的关键点如VNode、AST、createElement()等,我觉得还是值得深入了解的。写作不易,但如果觉得有收获,可以来个帅气的三冲程!!!