今年,疫情并无影响到各类面经的正常出现,可谓是络绎不绝(学不动...)。而后,在前段时间也看到一个这样的关于 Vue
的问题,为何每一个组件 template 中有且只能一个 root?javascript
可能,你们在日常开发中,用的较多就是 template
写 html
的形式。固然,不排除用 JSX
和 render()
函数的。可是,究其本质,它们最终都会转化成 render()
函数。而后,再由 render()
函数转为 Vritual DOM
(如下统称 VNode
)。而 render()
函数转为 VNode
的过程,是由 createElement()
函数完成的。html
所以,本次文章将会先讲述 Vue
为何限制 template
有且只能一个 root
。而后,再分析 Vue
如何规避出现多 root
的状况。那么,接下来咱们就从源码的角度去深究一下这个过程!vue
这里,咱们会分两个方面讲解,一方面是createElement()
的执行过程和定义,另外一方面是VNode
的定义。
createElement()
函数在源码中,被设计为 render()
函数的参数。因此官方文档也讲解了,如何使用 render()
函数的方式建立组件。 java
而 createElement()
会在 _render
阶段执行:node
... const { render, _parentVnode } = vm.$options ... vnode = render.call(vm._renderProxy, vm.$createElement);
能够很简单地看出,源码中经过 call()
将当前实例做为 context
上下文以及 $createElement
做为参数传入。数组
Vue2x 源码中用了大量的 call 和 apply,例如经典的 $set() API 实现数组变化的响应式处理就用的非常精妙,你们有兴趣能够看看。
$createElement
的定义又是这样:app
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
须要注意的是这个是咱们手写 render() 时调用的,若是是写 template 则会调用另外一个 vm._c 方法。二者的区别在于 createElement() 最后的参数前者为 true,后者为 false。
而到这里,这个 createElement()
实质是调用了 _createElement()
方法,它的定义:async
export function _createElement ( context: Component, // vm实例 tag?: string | Class<Component> | Function | Object, // DOM标签 data?: VNodeData, // vnode数据 children?: any, normalizationType?: number ): VNode | Array<VNode> { ... }
如今,见到了咱们日常使用的 createElement()
的庐山真面目。这里,咱们并不看函数内部的执行逻辑,这里分析一下这五个参数:ide
context
,是 Vue
在 _render
阶段传入的当前实例tag
,是咱们使用 createElement
时定义的根节点 HTML
标签名data
,是咱们使用 createElement
是传入的该节点的属性,例如 class
、style
、props
等等children
,是咱们使用 createElement
是传入的该节点包含的子节点,一般是一个数组normalizationType
,是用于判断拍平子节点数组时,要用简单迭代仍是递归处理,前者是针对简单二维,后者是针对多维。能够看出,createElement()
的设计,是针对一个节点,而后带 children
的组件的 VNode
的建立。而且,它并无留给你进行多 root
的建立的机会,只能传一个根 root
的 tag
,其余都是它的选项。函数
我想你们都知道 Vue2x
用的静态类型检测的方式是 flow
,因此它会借助 flow
实现自定义类型。而 VNode
就是其中一种。那么,咱们看看 VNode
类型定义:
前面,咱们分析了 createElement()
的调用时机,知道它最终返回的就是 VNode。那么,如今咱们来看看 VNode
的定义:
export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { ... } ... }
能够看到 VNode 所具有的属性仍是蛮多的,本次咱们就只看 VNode
前面三个属性:
显而易见的是 VNode
的设计也是一个 root
,而后由 children
不断延申下去。这样和前面 createElement()
的设计相呼应,不可能会出现多 root
的状况。
能够看到 VNode
和 createElement()
的设计,就只是针对单个 root
的状况进行处理,最终造成树的结构。那么,我想这个时候可能有人会问为何它们被设计树的结构?。
而针对这个问题,有两个方面,一方面是树形结构的 VNode
转为真实 DOM
后,咱们只须要将根 VNode
的真实 DOM
挂载到页面中。另外一方面是 DOM
自己就是树形结构,因此 VNode
也被设计为树形结构,并且以后咱们分析 template
编译阶段会提到 AST
抽象语法树,它也是树形结构。因此,统一的结构能够实现很方便的类型转化,即从 AST
到 Render
函数,从 Render
函数到 VNode
,最后从 VNode
到真实 DOM
。
而且,能够想一个情景,若是多个 root
,那么当你将 VNode
转为真实 DOM
时,挂载到页面中,是否是要遍历这个 DOM Collection
,而后挂载上去,而这个阶段又是操做 DOM
的阶段。你们都知道的一个东西就是操做 DOM
是很是昂贵的。因此,一个 root
的好处在这个时候就体现出它的好处了。
其实这个过程,让我想起 红宝书中在讲文档碎片的时候,提倡把要建立的 DOM 先添加到文档碎片中,而后将文档碎片添加到页面中。
在咱们日常的开发中,一般是在 .vue
文件中写 <template>
,而后经过在 <template>
中建立一个 div
来做为 root
,再在 root
中编写描述这个 .vue
文件的 html
标签。固然,你也能够直接写 render()
函数。
在文章的开始,咱们也说了在 Vue
中不管是写 template
仍是 render
,它最终会转成 render()
函数。而日常开发中,咱们用 template
的方式会较多。因此,这个过程就须要 Vue
来编译 template
。
编译 template
的这个过程会是这样:
template
生成 AST
(抽象语法树)AST
,即对 AST
节点进行静态节点或静态根节点的判断,便于以后 patch
判断AST
可执行的函数,在 Vue
中针对这一阶段定义了不少 _c
、_l
之类的函数,就其本质它们是对 render()
函数的封装这三个步骤在源码中的定义:
export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { // 生成 AST const ast = parse(template.trim(), options) if (options.optimize !== false) { // 优化 AST optimize(ast, options) } // 生成可执行的函数 const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })
须要注意的是Vue-CLI
提供了两个版本,Runtime-Compiler
和Runtime
,二者的区别,在于前者能够将template
编译成render()
函数,可是后者必须手写render()
函数
而对于开发中,若是你写了多个 root
的组件,在 parse
的时候,即生成 AST
抽象语法树的时候,Vue
就会过滤掉多余的 root
,只认第一个 root
。
而 parse
的整个过程,其实就是正则匹配的过程,而且这个过程会用栈来存储起始标签。整个 parse
过程的流程图:
而后,咱们经过一个例子来分析一下,其中针对多 root
的处理。假设此时咱们定义了这样的 template
:
<div><span></span></div><div></div>
显然,它是多 root
的。而在处理第一个 <div>
时,会建立对应的 ASTElement
,它的结构会是这样:
{ type: 1, tag: "div", attrsList: [], attrsMap: {}, rawAttrsMap: {}, parent: undefined, children: [], start: 0, end: 5 }
而此时,这个 ASTElement
会被添加到 stack
中,而后删除原字符串中的 <div>
,而且设置 root
为该 ASTElement
。
而后,继续遍历。对于 <span>
也会建立一个 ASTElement
并入栈,而后删除继续下一次。接下来,会匹配到 </span>
,此时会处理标签的结束,例如于栈顶 ASTElement
的 tag
进行匹配,而后出栈。接下来,匹配到 </div>
,进行和 span
一样的操做。
最后,对于第二个 root
的 <div>
,会作和上面同样的操做。可是,在处理 </div>
时,此时会进入判断 multiple root
的逻辑,即此时字符串已经处理完了,可是这个结束标签对应的 ASTElement
并不等于咱们最初定义的 root
。因此此时就会报错:
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.
并且,该 ASTElement
也不会加入最终的 AST
中,因此以后也不可能会出现多个 root
的状况。
同时,这个报错也提示咱们若是要用多个 root
,须要借助 if
条件判断来实现。
能够看出,template
编译的最终的目标就是构建一个 AST
抽象语法树。因此,它会在建立第一个 ASTElement
的时候就肯定 AST
的 root
,从而确保 root
惟一性。
不了解 Vue
初始化过程的同窗,可能不太清楚 _render
过程。你能够理解为渲染的过程。在这个阶段会调用 render
方法生成 VNode
,以及对 VNode
进行一些处理,最终返回一个 VNode
。
而相比较 template
编译的过程,_render
过程的判断就比较简洁:
if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ); } vnode = createEmptyVNode(); }
前面在讲 createElement
的时候,也讲到了 render()
须要返回 VNode
。因此,这里是防止部分骚操做,return
了包含多个 VNode
的数组。
经过阅读,我想你们也明白了为何 Vue 中 template 有且只能一个 root ?。Vue
这样设计的出发点可能很简单,为了减小挂载时 DOM
的操做。可是,它是如何处理多 root
的状况,以及相关的 VNode
、AST
、createElement()
等等关键点,我的认为都是很值得深刻了解的。
写做不易,若是你以为有收获的话,能够帅气三连击!!!