前言
Vue.js 提供了 2 个版本,一个是 Runtime + Compiler
版本,一个是 Runtime only
版本。Runtime + Compiler
版本是包含编译代码的,可以把编译过程放在运行时做,Runtime only
版本不包含编译代码的,需要借助 webpack 的 vue-loader 事先把模板编译成 render 函数。
如果你需要在客户端编译模板 (比如传入一个字符串给 template 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器,即完整版:
// 需要编译器 new Vue({ template: '<div>{{ hi }}</div>' }) // 不需要编译器 new Vue({ render (h) { return h('div', this.hi) } })
当使用 vue-loader 或 vueify 的时候,*.vue 文件内部的模板会在构建时预编译成 JavaScript。你在最终打好的包里实际上是不需要编译器的,所以只用运行时版本即可。因为运行时版本相比完整版体积要小大约 30%,所以应该尽可能使用这个版本。
在 Vue 的整个编译过程中,会做三件事:
- 解析模板
parse
,生成 AST - 优化 AST
optimize
- 生成代码
generate
对编译过程的了解会让我们对 Vue 的指令、内置组件等有更好的理解。不过由于编译的过程是一个相对复杂的过程,我们只要求理解整体的流程、输入和输出即可,对于细节我们不必抠太细。由于篇幅较长,这里会用三篇文章来讲这三件事。这是第一篇, 模板解析,template -> AST
。
注:全文源码来源,Vue(2.6.11),Runtime + Compiler 的 Vue.js
编译准备
这里先做一个准备工作,编译之前有一个嵌套的函数调用,看似非常的复杂,但是却有玄机。有什么玄机?接着往下看。
源码编译链式调用
compileToFunctions
在源码走了一遭,发现经过一系列的调用,最后 createCompiler
函数返回的 compileToFunctions
函数 对应的就是 $mount
函数调用的 compileToFunctions
方法,它是调用 createCompileToFunctionFn
方法的返回值。
// 伪代码 function createCompilerCreator (baseCompile) { return function createCompiler (baseOptions) { function compile ( template, options ) { ... return compiled } return { compile: compile, compileToFunctions: createCompileToFunctionFn(compile) } } } function createCompileToFunctionFn (compile) { var cache = Object.create(null); return function compileToFunctions ( template, options, vm ) { ... } }
方法接受三个参数。
- 编译模板 template
- 编译配置 options
- Vue 的实例
这个方法编译的核心代码就一行。
// compile var compiled = compile(template, options);
而 compile 方法的核心代码也就一行。
const compiled = baseCompile(template, finalOptions)
并且 baseCompile
方法是在执行 createCompilerCreator
方法执行的时候传入的。
var createCompiler = createCompilerCreator(function baseCompile ( template, options ) { var ast = parse(template.trim(), options); if (options.optimize !== false) { optimize(ast, options); } var code = generate(ast, options); return { ast: ast, render: code.render, staticRenderFns: code.staticRenderFns } });
baseCompile
会做三件事情。
其实看到这里你就会发现,这编译的准备工作,做了很多函数的调用,但是兜兜转转之后,最后回头来还是调用了最开始createCompilerCreator
传入的函数。
我理解这样做的原因是 Vue 本身是支持多平台的编译,在不同平台下的编译会有所有不同,但是在同一平台编译是相同的,所以在使用createCompiler(baseOptions)
时,baseOptions 会有所有不同。
在 Vue 中利用函数柯里化
的思想,将 baseOptions
的配置参数进行了保存。并且在调用链中,不断的进行函数调用并返回函数。
这其实也是利用了函数柯里化
的思想把很多基础的函数抽离出来, 通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。
编译准备已经做完,我们接下来看看 Vue 是如何做 parse
的。
parse
parse
要做的事情就是对 template 做解析,生成 AST 抽象语法树。
抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
例如现在有这样一段代码:
经过parse
,就变成了一个嵌套的树状结构的对象。
在 AST 中,每一个树节点都是一个 element,并且维护了上下文关系(父子关系)。
解析 template
parse
的过程核心就是 parseHTML
函数,这个函数的作用就是解析 template 模板。下面将解析过程中一些重要的点进行一个抽象解读。
function parseHTML (html, options) { var stack = []; ... // 遍历模板字符串 while (html) { ... } // 清除所有剩余的标签 parseEndTag(); // 将 html 字符串的指针前移 function advance (n) { ... } // 解析开始标签 function parseStartTag () { ... } // 处理解析的开始标签的结果 function handleStartTag (match) { ... } // 解析结束标签 function parseEndTag (tagName, start, end) { ... } }
标签匹配相关的正则
下面也会讲到关于一些指令匹配相关的正则。其实这些正则大家在平时的项目中有涉及也可以用起来,毕竟这些正则是经过千万人测试的。
// 识别合法的xml标签 var ncname = '[a-zA-Z_][\w\-\.]*'; // 复用拼接,这在我们项目中完成可以学起来 var qnameCapture = "((?:" + ncname + "\:)?" + ncname + ")"; // 匹配注释 var comment =/^<!--/; // 匹配<!DOCTYPE> 声明标签 var doctype = /^<!DOCTYPE [^>]+>/i; // 匹配条件注释 var conditionalComment =/^<![/; // 匹配开始标签 var startTagOpen = new RegExp(("^<" + qnameCapture)); // 匹配解说标签 var endTag = new RegExp(("^<\/" + qnameCapture + "[^>]*>")); // 匹配单标签 var startTagClose = /^\s*(/?)>/; // 匹配属性,例如 id、class var attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配动态属性,例如 v-if、v-else var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)[[^=]+][^\s"'<>/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
stack
变量 stack
,它定义一个栈,作用是存储开始标签。例如我有一个这样的简单模板:
- 1
当在 while 循环时,如果遇到一个非单标签,就会将开始标签 push 到数组中,遇到闭合标签就开始元素出栈,这样可以检测我们写的 template 是否符合嵌套、开闭规范,这也是检测 html 字符串中是否缺少闭合标签的原理。
advance
advance
函数贯穿这个 template 的解析流程。当我们在解析 template 字符串的时候,需要对字符串逐一扫描,直到结束。advance 函数的作用就是移动指针。例如匹配 <
字符,指针移动 1,匹配到