显而易见的是 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 = 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 。