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 方法。两者的区别在于 createElement() 最后的参数前者为 true,后者为 false。
而到这里,这个 createElement()
实质是调用了 _createElement()
方法,它的定义:
export function _createElement ( context: Component, // vm实例 tag"htmlcode">export default class VNode { tag: string | void; data: VNodeData | void; children: "text-align: center">并且,可以想一个情景,如果多个
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 = 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
唯一性。2.2 _render 过程
不了解
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()
等等关键点,个人认为都是很值得深入了解的。
下一篇:vue搜索页开发实例代码详解(热门搜索,历史搜索,淘宝接口演示)
人们对于笔记本电脑有一个固有印象:要么轻薄但性能一般,要么性能强劲但笨重臃肿。然而,今年荣耀新推出的MagicBook Pro 16刷新了人们的认知——发布会上,荣耀宣布猎人游戏本正式回归,称其继承了荣耀 HUNTER 基因,并自信地为其打出“轻薄本,更是游戏本”的口号。
众所周知,寻求轻薄本的用户普遍更看重便携性、外观造型、静谧性和打字办公等用机体验,而寻求游戏本的用户则普遍更看重硬件配置、性能释放等硬核指标。把两个看似难以相干的产品融合到一起,我们不禁对它产生了强烈的好奇:作为代表荣耀猎人游戏本的跨界新物种,它究竟做了哪些平衡以兼顾不同人群的各类需求呢?