1. 前言
在前几篇文章中,我们介绍了Vue
中的虚拟DOM
以及虚拟DOM
的patch
(DOM-Diff)过程,而虚拟DOM
存在的必要条件是得先有VNode
,那么VNode
又是从哪儿来的呢?这就是接下来几篇文章要说的模板编译。你可以这么理解:把用户写的模板进行编译,就会产生VNode
。
2. 什么是模板编译
我们知道,在日常开发中,我们把写在<template></template>
标签中的类似于原生HTML
的内容称之为模板。这时你可能会问了,为什么说是“类似于原生HTML
的内容”而不是“就是HTML
的内容”?因为我们在开发中,在<template></template>
标签中除了写一些原生HTML
的标签,我们还会写一些变量插值,如,或者写一些Vue
指令,如v-on
、v-if
等。而这些东西都是在原生HTML
语法中不存在的,不被接受的。但是事实上我们确实这么写了,也被正确识别了,页面也正常显示了,这又是为什么呢?
这就归功于Vue
的模板编译了,Vue
会把用户在<template></template>
标签中写的类似于原生HTML
的内容进行编译,把原生HTML
的内容找出来,再把非原生HTML
找出来,经过一系列的逻辑处理生成渲染函数,也就是render
函数,而render
函数会将模板内容生成对应的VNode
,而VNode
再经过前几篇文章介绍的patch
过程从而得到将要渲染的视图中的VNode
,最后根据VNode
创建真实的DOM
节点并插入到视图中, 最终完成视图的渲染更新。
而把用户在<template></template>
标签中写的类似于原生HTML
的内容进行编译,把原生HTML
的内容找出来,再把非原生HTML
找出来,经过一系列的逻辑处理生成渲染函数,也就是render
函数的这一段过程称之为模板编译过程。
3. 整体渲染流程
所谓渲染流程,就是把用户写的类似于原生HTML
的模板经过一系列处理最终反应到视图中称之为整个渲染流程。这个流程在上文中其实已经说到了,下面我们以流程图的形式宏观的了解一下,流程图如下:
从图中我们也可以看到,模板编译过程就是把用户写的模板经过一系列处理最终生成render
函数的过程。
4. 模板编译内部流程
那么模板编译内部是怎么把用户写的模板经过处理最终生成render
函数的呢?这内部的过程是怎样的呢?
4.1 抽象语法树AST
我们知道,用户在<template></template>
标签中写的模板对Vue
来说就是一堆字符串,那么如何解析这一堆字符串并且从中提取出元素的标签、属性、变量插值等有效信息呢?这就需要借助一个叫做抽象语法树的东西。
所谓抽象语法树,在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于if-condition-then这样的条件跳转语句,可以使用带有两个分支的节点来表示。——来自百度百科
我就知道,这段话贴出来也是白贴,因为看了也看不懂,哈哈。那么我们就以最直观的例子来理解什么是抽象语法树。请看下图:
从图中我们可以看到,一个简单的HTML
标签的代码被转换成了一个JS
对象,而这个对象中的属性代表了这个标签中一些关键有效信息。如图中标识。 有兴趣的同学可以在这个网站在线转换试试:https://astexplorer.net/
4.2 具体流程
将一堆字符串模板解析成抽象语法树AST
后,我们就可以对其进行各种操作处理了,处理完后用处理后的AST
来生成render
函数。其具体流程可大致分为三个阶段:
- 模板解析阶段:将一堆模板字符串用正则等方式解析成抽象语法树
AST
; - 优化阶段:遍历
AST
,找出其中的静态节点,并打上标记; - 代码生成阶段:将
AST
转换成渲染函数;
这三个阶段在源码中分别对应三个模块,下面给出三个模块的源代码在源码中的路径:
- 模板解析阶段——解析器——源码路径:
src/compiler/parser/index.js
; - 优化阶段——优化器——源码路径:
src/compiler/optimizer.js
; - 代码生成阶段——代码生成器——源码路径:
src/compiler/codegen/index.js
; 其对应的源码如下:
// 源码位置: /src/complier/index.js export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { // 模板解析阶段:用正则等方式解析 template 模板中的指令、class、style等数据,形成AST const ast = parse(template.trim(), options) if (options.optimize !== false) { // 优化阶段:遍历AST,找出其中的静态节点,并打上标记; optimize(ast, options) } // 代码生成阶段:将AST转换成渲染函数; const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })
可以看到 baseCompile
的代码非常的简短主要核心代码。
- const ast =parse(template.trim(), options):
parse
会用正则等方式解析template
模板中的指令、class
、style
等数据,形成AST
。 - optimize(ast, options):
optimize
的主要作用是标记静态节点,这是Vue
在编译过程中的一处优化,挡在进行patch
的过程中,DOM-Diff
算法会直接跳过静态节点,从而减少了比较的过程,优化了patch
的性能。 - const code =generate(ast, options): 将
AST
转化成render
函数字符串的过程,得到结果是render
函数 的字符串以及staticRenderFns
字符串。
最终 baseCompile
的返回值
{ ast: ast, render: code.render, staticRenderFns: code.staticRenderFns }
最终返回了抽象语法树( ast ),渲染函数( render ),静态渲染函数( staticRenderFns ),且render
的值为code.render
,staticRenderFns
的值为code.staticRenderFns
,也就是说通过 generate
处理 ast
之后得到的返回值 code
是一个对象。
下面再给出模板编译内部具体流程图,便于理解。流程图如下:
5. 小总结
本篇文章首先引出了为什么会有模板编译,因为有了模板编译,才有了虚拟DOM
,才有了后续的视图更新。接着介绍了什么是模板编译,以及介绍了把用户所写的模板经过层层处理直到最终渲染的视图中这个整体的渲染流程;最后介绍了模板编译过程中所需要使用的抽象语法树的概念以及分析了模板编译的具体实施流程,其流程大致分为三个阶段,分别是模板解析阶段、优化阶段和代码生成阶段。那么接下来的几篇文章将会把这三个阶段逐一进行分析介绍。
6. 整体流程
前言中我们说了,在模板解析阶段主要做的工作是把用户在<template></template>
标签内写的模板使用正则等方式解析成抽象语法树(AST
)。而这一阶段在源码中对应解析器(parser
)模块。
解析器,顾名思义,就是把用户所写的模板根据一定的解析规则解析出有效的信息,最后用这些信息形成AST
。我们知道在<template></template>
模板内,除了有常规的HTML
标签外,用户还会一些文本信息以及在文本信息中包含过滤器。而这些不同的内容在解析起来肯定需要不同的解析规则,所以解析器不可能只有一个,它应该除了有解析常规HTML
的HTML解析器,还应该有解析文本的文本解析器以及解析文本中如果包含过滤器的过滤器解析器。
另外,文本信息和标签属性信息却又是存在于HTML标签之内的,所以在解析整个模板的时候它的流程应该是这样子的:HTML解析器是主线,先用HTML解析器进行解析整个模板,在解析过程中如果碰到文本内容,那就调用文本解析器来解析文本,如果碰到文本中包含过滤器那就调用过滤器解析器来解析。如下图所示:
7. 回到源码
解析器的源码位于/src/complier/parser
文件夹下,其主线代码如下:
// 代码位置:/src/complier/parser/index.js /** * Convert HTML string to AST. */ export function parse(template, options) { // ... parseHTML(template, { warn, expectHTML: options.expectHTML, isUnaryTag: options.isUnaryTag, canBeLeftOpenTag: options.canBeLeftOpenTag, shouldDecodeNewlines: options.shouldDecodeNewlines, shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, shouldKeepComment: options.comments, start (tag, attrs, unary) { }, end () { }, chars (text: string) { }, comment (text: string) { } }) return root }
从上面代码中可以看到,parse
函数就是解析器的主函数,在parse
函数内调用了parseHTML
函数对模板字符串进行解析,在parseHTML
函数解析模板字符串的过程中,如果遇到文本信息,就会调用文本解析器parseText
函数进行文本解析;如果遇到文本中包含过滤器,就会调用过滤器解析器parseFilters
函数进行解析。
8. 总结
本篇文章主要梳理了模板解析的整体运行流程,模板解析其实就是根据被解析内容的特点使用正则等方式将有效信息解析提取出来,根据解析内容的不同分为HTML解析器,文本解析器和过滤器解析器。而文本信息与过滤器信息又存在于HTML标签中,所以在解析器主线函数parse
中先调用HTML解析器parseHTML
函数对模板字符串进行解析,如果在解析过程中遇到文本或过滤器信息则再调用相应的解析器进行解析,最终完成对整个模板字符串的解析。
了解了模板解析阶段的整体运行流程后,接下来,我们就对流程中所涉及到的三种解析器分别深入分析,逐个击破。