Vue中template有且只能⼀个root的原因解析(源码分
析)
引⾔
今年,疫情并没有影响到各种⾯经的正常出现,可谓是络绎不绝(学不动...)。然后,在前段时间也看到⼀个这样的关于Vue 的问题,为什么每个组件 template 中有且只能⼀个 root?
可能,⼤家在平常开发中,⽤的较多就是template写html的形式。当然,不排除⽤JSX和render()函数的。但是,究其本质,它们最终都会转化成render()函数。然后,再由render()函数转为Vritual DOM(以下统称VNode)。⽽render()函数转为VNode的过程,是由createElement()函数完成的。
因此,本次⽂章将会先讲述Vue为什么限制template有且只能⼀个root。然后,再分析Vue如何规避出现多root的情况。那么,接下来我们就从源码的⾓度去深究⼀下这个过程!
⼀、为什么限制 template 有且只能有⼀个 root
这⾥,我们会分两个⽅⾯讲解,⼀⽅⾯是createElement()的执⾏过程和定义,另⼀⽅⾯是VNode的定义。
1.1 createElement()
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() 时调⽤的,如果是写 template 则会调⽤另⼀个 vm._c ⽅法。两者的区别在于creat
eElement() 最后的参数前者为 true,后者为 false。
⽽到这⾥,这个createElement()实质是调⽤了_createElement()⽅法,它的定义:
export function _createElement (
context: Component, // vm实例
tag?: string | Class<Component> | Function | Object, // DOM标签
data?: VNodeData, // vnode数据
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...
}
现在,见到了我们平常使⽤的createElement()的庐⼭真⾯⽬。这⾥,我们并不看函数内部的执⾏逻辑,这⾥分析⼀下这五个参数:
context,是Vue在_render阶段传⼊的当前实例
tag,是我们使⽤createElement时定义的根节点HTML标签名
data,是我们使⽤createElement是传⼊的该节点的属性,例如class、style、props等等
children,是我们使⽤createElement是传⼊的该节点包含的⼦节点,通常是⼀个数组
normalizationType,是⽤于判断拍平⼦节点数组时,要⽤简单迭代还是递归处理,前者是针对简单⼆维,后者是针对多维。
可以看出,createElement()的设计,是针对⼀个节点,然后带children的组件的VNode的创建。并且,它并没有留给你进⾏多root的创建的机会,只能传⼀个根root的tag,其他都是它的选项。
1.2 VNode
我想⼤家都知道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前⾯三个属性:
tag,即 VNode 对于的标签名
data,即 VNode 具备的⼀些属性
children,即 VNode 的⼦节点,它是⼀个 VNode 数组
显⽽易见的是VNode的设计也是⼀个root,然后由children不断延申下去。这样和前⾯createElement()的设计相呼应,不可能会出现多root的情况。
正则匹配一个或连续多个1.3 ⼩结
可以看到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 先添加到⽂档碎⽚中,然后将⽂档碎⽚添加到页⾯中。(PS:想想第⼀次看是去年 4 ⽉份,刚开始学前端,不经意间过了快⼀年了....)
⼆、如何规避出现多 root 的情况
2.1 template 编译过程
在我们平常的开发中,通常是在.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 = im(), options)
if (options.optimize !== false) {
// 优化 AST
optimize(ast, options)
}
// ⽣成可执⾏的函数
const code = generate(ast, options)
return {
ast,
render: der,
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唯⼀性。
2.2 _render 过程
不了解Vue初始化过程的同学,可能不太清楚_render过程。你可以理解为渲染的过程。在这个阶段会调⽤render⽅法⽣成VNode,以及对VNode进⾏⼀些处理,最终返回⼀个VNode。
⽽相⽐较template编译的过程,_render过程的判断就⽐较简洁:
if (!(vnode instanceof VNode)) {
if (v.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()等等关键点,个⼈认为都是很值得深⼊了解的。
到此这篇关于Vue 中 template 有且只能⼀个 root的原因解析(源码分析)的⽂章就介绍到这了,更多相关vue template 有且只能⼀个 root内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论