引言引言引言今年, 疫情 并没有影响到各种面经的正常出现,可谓是络绎不绝(学不动...)。然后,在前段时间也看到一个这样的关于 Vue 的问题, 为什么每个组件 template 中有且只能一个 root?疫情Vue为什么每个组件 template 中有且只能一个 root?可能,大家在平常开发中,用的较多就是 template 写 html 的形式。当然,不排除用 JSX 和 render() 函数的。但是,究其本质,它们最终都会转化成 render() 函数。然后,再由 render() 函数转为 Vritual DOM (以下统称 VNode )。而 render() 函数转为 VNode 的过程,是由 createElement() 函数完成的。templatehtmlJSXrender()render()render()Vritual DOMVNoderender()VNodecreateElement()因此,本次文章将会先讲述 Vue 为什么限制 template 有且只能一个 root 。然后,再分析 Vue 如何规避出现多 root 的情况。那么,接下来我们就从源码的角度去深究一下这个过程!VuetemplaterootVueroot一、为什么限制 template 有且只能有一个 root一、为什么限制 template 有且只能有一个 root一、为什么限制 template 有且只能有一个 root这里,我们会分两个方面讲解,一方面是 createElement() 的执行过程和定义,另一方面是 VNode 的定义。createElement()VNode1.1 createElement()1.1 createElement()createElement() 函数在源码中,被设计为 render() 函数的参数。所以 官方文档 也讲解了,如何使用 render() 函数的方式创建组件。createElement()render()render()而 createElement() 会在 _render 阶段执行:createElement()_render
...
const { render, _parentVnode } = vm.$options
...
vnode = render.call(vm._renderProxy, vm.$createElement);
...
const { render, _parentVnode } = vm.$options
...
vnode = render.call(vm._renderProxy, vm.$createElement);可以很简单地看出,源码中通过 call() 将当前实例作为 context 上下文以及 $createElement 作为参数传入。call()context$createElementVue2x 源码中用了大量的 call 和 apply,例如经典的 $set() API 实现数组变化的响应式处理就用的很是精妙,大家有兴趣可以看看。$createElement 的定义又是这样:$createElement
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)需要注意的是这个是我们手写 render() 时调用的,如果是写 template 则会调用另一个 vm._c 方法。两者的区别在于 createElement() 最后的参数前者为 true,后者为 false。而到这里,这个 createElement() 实质是调用了 _createElement() 方法,它的定义:createElement()_createElement()
export function _createElement (
context: Component, // vm实例
tag?: string | Class | Function | Object, // DOM标签
data?: VNodeData, // vnode数据
children?: any,
normalizationType?: number
): VNode | Array {
...
}
export function _createElement (
context: Component, // vm实例
tag?: string | Class | Function | Object, // DOM标签
data?: VNodeData, // vnode数据
children?: any,
normalizationType?: number
): VNode | Array {
...
}现在,见到了我们平常使用的 createElement() 的 庐山真面目 。这里,我们并不看函数内部的执行逻辑,这里分析一下这五个参数:createElement()庐山真面目

context ,是 Vue 在 _render 阶段传入的当前实例

tag ,是我们使用 createElement 时定义的根节点 HTML 标签名

data ,是我们使用 createElement 是传入的该节点的属性,例如 class 、 style 、 props 等等

children ,是我们使用 createElement 是传入的该节点包含的子节点,通常是一个数组

normalizationType ,是用于判断拍平子节点数组时,要用简单迭代还是递归处理,前者是针对简单二维,后者是针对多维。
context ,是 Vue 在 _render 阶段传入的当前实例contextVue_rendertag ,是我们使用 createElement 时定义的根节点 HTML 标签名tagcreateElementHTMLdata ,是我们使用 createElement 是传入的该节点的属性,例如 class 、 style 、 props 等等datacreateElementclassstylepropschildren ,是我们使用 createElement 是传入的该节点包含的子节点,通常是一个数组childrencreateElementnormalizationType ,是用于判断拍平子节点数组时,要用简单迭代还是递归处理,前者是针对简单二维,后者是针对多维。normalizationType可以看出, createElement() 的设计,是针对一个节点,然后带 children 的组件的 VNode 的创建。并且,它并没有留给你进行多 root 的创建的机会,只能传一个根 root 的 tag ,其他都是它的选项。createElement()childrenVNoderootroottag1.2 VNode1.2 VNode我想大家都知道 Vue2x 用的静态类型检测的方式是 flow ,所以它会借助 flow 实现自定义类型。而 VNode 就是其中一种。那么,我们看看 VNode 类型定义:Vue2xflowflowVNodeVNode前面,我们分析了 createElement() 的调用时机,知道它最终返回的就是 VNode。那么,现在我们来看看 VNode 的定义:createElement()VNode
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array;
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,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
...
}
...
}
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array;
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,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
...
}
...
}可以看到 VNode 所具备的属性还是蛮多的,本次我们就只看 VNode 前面三个属性:VNode

tag,即 VNode 对于的标签名

data,即 VNode 具备的一些属性

children,即 VNode 的子节点,它是一个 VNode 数组
tag,即 VNode 对于的标签名data,即 VNode 具备的一些属性children,即 VNode 的子节点,它是一个 VNode 数组显而易见的是 VNode 的设计也是一个 root ,然后由 children 不断延申下去。这样和前面 createElement() 的设计相呼应, 不可能会 出现多 root 的情况。VNoderootchildrencreateElement()不可能会root1.3 小结1.3 小结可以看到 VNode 和 createElement() 的设计,就只是针对单个 root 的情况进行处理,最终形成 树的结构 。那么,我想这个时候 可能有人会问为什么它们被设计树的结构? 。VNodecreateElement()root树的结构可能有人会问为什么它们被设计树的结构?而针对这个问题,有 两个方面 ,一方面是树形结构的 VNode 转为真实 DOM 后,我们只需要将根 VNode 的真实 DOM 挂载到页面中。另一方面是 DOM 本身就是树形结构,所以 VNode 也被设计为树形结构,而且之后我们分析 template 编译阶段会提到 AST 抽象语法树,它也是树形结构。所以,统一的结构可以实现很方便的类型转化,即从 AST 到 Render 函数,从 Render 函数到 VNode ,最后从 VNode 到真实 DOM 。两个方面VNodeDOMVNodeDOMDOMVNodetemplateASTASTRenderRenderVNodeVNodeDOM并且,可以想一个情景,如果多个 root ,那么当你将 VNode 转为真实 DOM 时,挂载到页面中,是不是要遍历这个 DOM Collection ,然后挂载上去,而这个阶段又是操作 DOM 的阶段。大家都知道的一个东西就是操作 DOM 是 非常昂贵的 。所以,一个 root 的好处在这个时候就体现出它的好处了。rootVNodeDOMDOM CollectionDOMDOM非常昂贵的root其实这个过程,让我想起 红宝书 中在讲文档碎片的时候,提倡把要创建的 DOM 先添加到文档碎片中,然后将文档碎片添加到页面中。(PS:想想第一次看红宝书是去年 4 月份,刚开始学前端,不经意间过了快一年了....)红宝书二、如何规避出现多 root 的情况二、如何规避出现多 root 的情况二、如何规避出现多 root 的情况 2.1 template 编译过程2.1 template 编译过程在我们平常的开发中,通常是在 .vue 文件中写