Vue 中 template 有且只能一个 root的原因解析(源码分析)

引言

今年, 疫情 并没有影响到各种面经的正常出现,可谓是络绎不绝(学不动...)。然后,在前段时间也看到一个这样的关于 Vue 的问题, 为什么每个组件 template 中有且只能一个 root?

可能,大家在平常开发中,用的较多就是 templatehtml 的形式。当然,不排除用 JSXrender() 函数的。但是,究其本质,它们最终都会转化成 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?: 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 是传入的该节点的属性,例如 classstyleprops 等等
  • children ,是我们使用 createElement 是传入的该节点包含的子节点,通常是一个数组
  • normalizationType ,是用于判断拍平子节点数组时,要用简单迭代还是递归处理,前者是针对简单二维,后者是针对多维。

可以看出, createElement() 的设计,是针对一个节点,然后带 children 的组件的 VNode 的创建。并且,它并没有留给你进行多 root 的创建的机会,只能传一个根 roottag ,其他都是它的选项。

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 小结

可以看到 VNodecreateElement() 的设计,就只是针对单个 root 的情况进行处理,最终形成 树的结构 。那么,我想这个时候 可能有人会问为什么它们被设计树的结构?

而针对这个问题,有 两个方面 ,一方面是树形结构的 VNode 转为真实 DOM 后,我们只需要将根 VNode 的真实 DOM 挂载到页面中。另一方面是 DOM 本身就是树形结构,所以 VNode 也被设计为树形结构,而且之后我们分析 template 编译阶段会提到 AST 抽象语法树,它也是树形结构。所以,统一的结构可以实现很方便的类型转化,即从 ASTRender 函数,从 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 = 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-CompilerRuntime ,两者的区别,在于前者可以将 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> ,此时会处理标签的结束,例如于栈顶 ASTElementtag 进行匹配,然后出栈。接下来,匹配到 </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 的时候就确定 ASTroot ,从而确保 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 的情况,以及相关的 VNodeASTcreateElement() 等等关键点,个人认为都是很值得深入了解的。

到此这篇关于Vue 中 template 有且只能一个 root的原因解析(源码分析)的文章就介绍到这了,更多相关vue template 有且只能一个 root内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • vue template中slot-scope/scope的使用方法

    在vue 2.5.0+ 中slot-scope替代了 scope template 的使用情形为,我们已经封装好一个组建,预留了插槽,使用 的插槽 首先 我们的创建一个组建 组建很简单有一个 slot,slot有两个属性 a=123,b=msg <template> <div> <div>下面是一个slot</div> <slot a="123" b="msg" ></slot> </di

  • Vue2 模板template的四种写法总结

    如下所示: <div id="app"> <h1>我是直接写在构造器里的模板1</h1> </div> <template id="demo3"> <h1 style="color:red">我是选项模板3</h1> </template> <script type="x-template" id="demo4&qu

  • 简单理解vue中el、template、replace元素

    本文实例为大家解析了vue中el.template.replace的元素,供大家参考,具体内容如下 api: http://cn.vuejs.org/api/#el el 类型: String | HTMLElement | Function 限制: 在组件定义中只能是函数. 详细: 为实例提供挂载元素.值可以是 CSS 选择符,或实际 HTML 元素,或返回 HTML 元素的函数.注意元素只用作挂载点.如果提供了模板则元素被替换,除非 replace 为 false.元素可以用 vm.$el

  • Vue.js学习记录之在元素与template中使用v-if指令实例

    本文主要给大家介绍的是关于Vue.js在元素与template中使用v-if指令的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 语法比较简单,直接上代码: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>&

  • 详解使用vue-admin-template的优化历程

    前言 公司有好几个项目都有后台管理系统,为了方便开发,所以选择了 vue 中比较火的后台模板作为基础模板进行开发.但是,开始用的时候,作者并没有对此进行优化,到项目上线的时候,才发现,打包出来的文件都十分之大,就一个 vendor 就有 770k 的体积(下图是基础模板,什么都没加打包后的文件信息): 通过 webpack-bundle-analyzer 进行分析可得,体积主要来源于饿了么UI(体积为 500k),因为没对其进行部分引入拆分组件,导致 webpack 把整个组件库都打包进去了.其

  • vue.js template模板的使用(仿饿了么布局)

    使用template实现如下页面(仿饿了么布局) 如上图.使用了4个组件,分别是header.vue,goods.vue,ratings.vue,seller.vue header.vue代码如下 <template> <div class="header"> 我是header头部 </div> </template> <script type="text/ecmascript-6"> export def

  • Vue 中 template 有且只能一个 root的原因解析(源码分析)

    引言 今年, 疫情 并没有影响到各种面经的正常出现,可谓是络绎不绝(学不动...).然后,在前段时间也看到一个这样的关于 Vue 的问题, 为什么每个组件 template 中有且只能一个 root? 可能,大家在平常开发中,用的较多就是 template 写 html 的形式.当然,不排除用 JSX 和 render() 函数的.但是,究其本质,它们最终都会转化成 render() 函数.然后,再由 render() 函数转为 Vritual DOM (以下统称 VNode ).而 rende

  • 解决vue中监听input只能输入数字及英文或者其他情况的问题

    如下所示: 1. <input placeholder="请输入条形码" class="disPriceStyle" v-model = 'searcBarCode'></input>. (只能输入数字,英文) <input placeholder="请输入商品条形码" class="disPriceStyle" maxlength="20" v-model = 'search

  • Vue组件中的data必须是一个function的原因浅析

    组件可以有自己的data,并且data必须是一个function. 在下面这个例子中,data 返回了一个在外部定义的对象.并且这个组件在页面中使用了3次,点击+1时出现了如下情况:data中的count属性影响到了所有实例. <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link rel="

  • 在vue中动态修改css其中一个属性值操作

    我就废话不多说了,大家还是直接看代码吧~ <template> <!--此div的高度取屏幕可视区域的高度--> <div class="hello" :style="{'height':getClientHeight}"> <h5>{{ msg }}</h5> </div> </template> <script> export default { data() { r

  • vue中template的三种写法示例

    第一种(字符串模板写法): 直接写在vue构造器里,这种写法比较直观,适用于html代码不多的场景,但是如果模板里html代码太多,不便于维护,不建议这么写. <!DOCTYPE html> <html> <!-- WARNING! Make sure that you match all Quasar related tags to the same version! (Below it's "@1.7.4") --> <head> &

  • VUE中template的三种写法

    一.直接写在构造器中 <!-- 第一种写法:直接写在构造器里 --> <div id ="app1"> </div> <script> var vm1 = new Vue({ el: '#app1', data: {}, methods: {}, template:`<h3>在构造器中的文字</h3>` }); </script> 二.写在HTML自带的<template>标签中 <!

  • Vue中的数据监听和数据交互案例解析

    现在我们来看一下vue中的数据监听事件$watch, js代码: new Vue({ el:"#div", data:{ arr:[1,2,3] } }).$watch("arr",function () { alert("数据改变了") }) html代码: <div id="div"> <input type="button" value="改变" @click=&

  • Vue中 v-if/v-show/插值表达式导致闪现的原因及解决办法

    在开发过程中经常会发现当页面明明不应该出现的元素或内容会闪现一下然后消失,最近研究了一下这个问题的原因和解决办法,这里和大家分享一下. 1.闪现的原因 这个问题是因为Vue要等到HTML DOM加载完成后才会执行JS的编译,所以对使用的指令如 v-if , v-show 或者使用了插值表达式 {{}} 都会出现闪现的情况.因为在这些判断条件或表达式执行之前,DOM已经渲染出来了,之后Vue才会执行相应的JS代码. 2.解决的办法 其实在了解了原因之后我们就有了大概的思路,既然是在JS执行之前会出

  • Vue编译器optimize源码分析

    目录 引言 optimize 源码之旅 markStatic$1源码 isStatic源码 复杂点的 回归到markStatic$1 markStaticRoots 源码 引言 接上文 parseHTML 函数源码解析 chars.end.comment钩子函数 上一章节我们讲到通过解析将template转成AST(抽象语法树),接下来继续对模型树优化,进行静态标注.那么问题来了,什么是静态标注?为什么要静态标注. 在源码的注释中我们找到了下面这段话: /** * Goal of the opt

  • Vue编译器AST抽象语法树源码分析

    目录 引言 baseCompile主要核心代码 如何写一个程序来识别 Token parse 函数解析模板字符串 引言 接上篇  Vue编译器源码分析compile 解析 baseCompile主要核心代码 // `createCompilerCreator` allows creating compilers that use alternative // parser/optimizer/codegen, e.g the SSR optimizing compiler. // Here we

随机推荐