Vue编程三部曲之将template编译成AST示例详解

前言

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

编译准备

这里先做一个准备工作,编译之前有一个嵌套的函数调用,看似非常的复杂,但是却有玄机。有什么玄机?接着往下看。

源码编译链式调用

Vue编程三部曲之将template编译成AST示例详解_第1张图片

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,就变成了一个嵌套的树状结构的对象。

Vue编程三部曲之将template编译成AST示例详解_第2张图片

在 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 字符串中是否缺少闭合标签的原理。

Vue编程三部曲之将template编译成AST示例详解_第3张图片

advance

advance 函数贯穿这个 template 的解析流程。当我们在解析 template 字符串的时候,需要对字符串逐一扫描,直到结束。advance 函数的作用就是移动指针。例如匹配 <字符,指针移动 1,匹配到