Skip to content
微信公众号

mpvue-template-compiler 源码分析

mpvue-template-compiler 引用流程分析

mpvue-template-compiler 的引用位于 mpvue-loader/lib/template-compiler/index.js 中:

js
var compiler = require('mpvue-template-compiler')

通过 compiler 获取 compile 函数:

js
var compile = isServer && compiler.ssrCompile && vueOptions.optimizeSSR !== false
    ? compiler.ssrCompile
    : compiler.compile

小程序是非 SSR 场景,所以 compile 函数为 compiler.compile,通过 compile 实现对 template 的解析:

js
var compiled = compile(html, compilerOptions)

这里的 html 就是 template 字符串,通过 compile 函数会生成 ast 和 render 函数,ast 是抽象代码树的含义,它会将 html 标签解析为一个 js 对象,通过该对象最终生成 render 函数,执行 render 会生成 vnode 对象,该 vnode 对象对应 template 的结构。我们仍然采用上节的案例,源码如下:

vue
<template>
  <div>{{message}}</div>
</template>

<script>
export default {
  data () {
    return {
      message: 'Hello World'
    }
  }
}
</script>

<style scoped>
div {
  color: red;
}
</style>

该案例中的 template 字符串为:<div></div>,经过 compile 编译后的对象为:

js
{
  "ast": {
    "type": 1,
    "tag": "div",
    "attrsList": [],
    "attrsMap": {},
    "children": [
      {
        "type": 2,
        "expression": "_s(message)",
        "text": "{{message}}",
        "static": false
      }
    ],
    "plain": true,
    "static": false,
    "staticRoot": false
  },
  "render": "with(this){return _c('div',[_v(_s(message))])}",
  "staticRenderFns": [],
  "errors": [],
  "tips": []
}

这里我们重点看 ast 对象,它的主要属性含义如下:

  • type:html 元素对应的 nodeType,这里 type 为 1 表示节点类型为 Element 即元素;
  • tag:对应 html 标签名称,这里是 div;
  • attrsList 和 attrsMap:对应标签内的属性列表和属性对象,这里由于 div 内没有属性,所以为空数组和空对象;
  • children:html 元素内部的子元素,由此可看出 ast 是一个树状结果,根节点下将包含子元素,子元素中将包含孙元素,以此类推;children 是一个数组,因为 div 内可以包含多个 DOM,这里的 children 即 ,static 属性代表是否为静态属性,如果是静态属性,在 render 的 diff 算法时会被略过,但是小程序使用 setData 做状态更新和渲染,所以不会涉及这个属性。

生成了 ast 之后,mpvue-loader 会继续执行下面的代码:

js
compileMPML.call(this, compiled, html, options)

compileMPML 是实际生成 wxml 文件的方法,我们将在下一节中分析这个方法,本节我们只需要知道它会利用 compile 函数生成的 compiled 对象即可。通过上述分析可以看到 mpvue-template-compiler 库最重要的作用就是生成 ast 和 render 函数,下面我们就一同查看 compile 函数的实现。

mpvue-template-compiler 源码分析

mpvue-template-compiler 的源码位于 node_modules/mpvue-template-compiler/build.js 中,将代码拖动到底部,可以看到 compile 的定义:

js
var ref = createCompiler(baseOptions);
var compile = ref.compile;

exports.compile = compile;

compile 来自 ref.compile,而 ref 由 createCompiler 函数而来,createCompiler 生成了一个对象,对象中包含了 compile 函数,继续查看 createCompiler 的定义:

js
var createCompiler = createCompilerCreator(function baseCompile() {});

这里为了避免大家混淆,我将 baseCompile 的内容暂时删除,后续再进行分析,createCompiler 执行了 createCompilerCreator 函数,createCompilerCreator 函数定义如下:

js
function createCompilerCreator (baseCompile) {
  return function createCompiler (baseOptions) {
    function compile (
      template$$1,
      options
    ) {
      // ...
      var compiled = baseCompile(template$$1, finalOptions);
      if (process.env.NODE_ENV !== 'production') {
        errors.push.apply(errors, detectErrors(compiled.ast));
      }
      compiled.errors = errors;
      compiled.tips = tips;
      return compiled
    }

    return {
      compile: compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

这里用到了函数柯里化,createCompilerCreator 执行后会返回一个函数 createCompiler,createCompiler 函数执行会返回一个对象,对象结构如下:

js
{
  compile: compile,
  compileToFunctions: createCompileToFunctionFn(compile)
}

该对象即 ref,执行 compile 函数实际调用的是 createCompiler 中 compile,而 compile 中的核心逻辑是:

js
var compiled = baseCompile(template$$1, finalOptions);

这里的 baseCompile 即 createCompilerCreator 传入的参数,可见真正完成 template 编译的是 baseCompile 函数。

baseCompile 源码分析

baseCompile 源码如下:

js
function baseCompile (
  template,
  options
) {
  var originAst = parse(template.trim(), options);
  var ast = markComponent(originAst, options);
  optimize(ast, options);
  var code = generate(ast, options);
  return {
    ast: ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

baseCompile 共完成了以下三件事:

  • 生成 ast:通过 parse 方法生成 ast;
  • 优化 ast:通过 optimize 优化 ast,主要是对 ast 中的静态节点进行识别和标记;
  • 生成 render 函数:通过 generate 方法将 ast 转化为 render 函数。

这里重点分析 template 生成 ast 的过程即 parse 方法,parse 方法十分复杂,它的主要结构如下:

js
function parse (
  template,
  options
) {
  warn = options.warn || baseWarn;

  platformIsPreTag = options.isPreTag || no;
  platformMustUseProp = options.mustUseProp || no;
  platformGetTagNamespace = options.getTagNamespace || no;

  transforms = pluckModuleFunction(options.modules, 'transformNode');
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');

  delimiters = options.delimiters;

  var stack = [];
  var preserveWhitespace = options.preserveWhitespace !== false;
  var root;
  var currentParent;
  var inVPre = false;
  var inPre = false;
  var warned = false;

  function warnOnce (msg) {
  }

  function endPre (element) {
  }

  parseHTML(template, {});
  return root
}

这里省略了具体实现,只包含了主干,parse 最终返回值是 root,可见 root 就是最终返回的 ast 对象,而 parseHTML 就是具体生成 ast 的方法,具体解析流程如下,这里以 <div></div> 为例,通过 parseHTML 流程的分析,我们可以体会 HTML 解析的原理:

  • 第一步,parseHTML 会通过正则表达式匹配 <,一旦匹配到 <,它会继续通过正则匹配其中的内容,这里会取出 div,之后 parseHTML 通过正则循环匹配 div 标签的属性,这里由于 div 中没有属性,所以会略过,循环匹配的原因是因为 div 中可能包含多个属性。属性匹配完毕后会继续匹配 >,至此 HTML 的第一个标签匹配完毕;
  • 第二步,parseHTML 会继续匹配下一个 <,匹配到下一个 < 后,< 之前的内容就是属性的值,会存入 ast 属性的 children 中;
  • 第三步,匹配到下一个 < 后,parseHTML 会匹配标签,如果发现是闭合标签,即包含 / 的标签,则会结束解析过程。

通过上述过程最终会得到一个根节点为 div 的 ast,其中包含一个 children,children 的内容为 。这里 parseHTML 的逻辑与 Vue 完全一致,因为小程序的 template 也是类 HTML 格式,所以解析流程是一致的。

本站总访问量次,本站总访客数人次
Released under the MIT License.