当前位置: 首页 > Web前端 > vue.js

面试官:为什么Vue中的模板有且只有一个根?(深度解读)

时间:2023-03-31 23:55:39 vue.js

简介前段时间看到这样一个关于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文件中写