mpvue-template-compiler 源码分析
mpvue-template-compiler 引用流程分析
mpvue-template-compiler 的引用位于 mpvue-loader/lib/template-compiler/index.js 中:
var compiler = require('mpvue-template-compiler')
通过 compiler 获取 compile 函数:
var compile = isServer && compiler.ssrCompile && vueOptions.optimizeSSR !== false
? compiler.ssrCompile
: compiler.compile
小程序是非 SSR 场景,所以 compile 函数为 compiler.compile,通过 compile 实现对 template 的解析:
var compiled = compile(html, compilerOptions)
这里的 html 就是 template 字符串,通过 compile 函数会生成 ast 和 render 函数,ast 是抽象代码树的含义,它会将 html 标签解析为一个 js 对象,通过该对象最终生成 render 函数,执行 render 会生成 vnode 对象,该 vnode 对象对应 template 的结构。我们仍然采用上节的案例,源码如下:
<template>
<div>{{message}}</div>
</template>
<script>
export default {
data () {
return {
message: 'Hello World'
}
}
}
</script>
<style scoped>
div {
color: red;
}
</style>
该案例中的 template 字符串为:<div></div>
,经过 compile 编译后的对象为:
{
"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 会继续执行下面的代码:
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 的定义:
var ref = createCompiler(baseOptions);
var compile = ref.compile;
exports.compile = compile;
compile 来自 ref.compile,而 ref 由 createCompiler 函数而来,createCompiler 生成了一个对象,对象中包含了 compile 函数,继续查看 createCompiler 的定义:
var createCompiler = createCompilerCreator(function baseCompile() {});
这里为了避免大家混淆,我将 baseCompile 的内容暂时删除,后续再进行分析,createCompiler 执行了 createCompilerCreator 函数,createCompilerCreator 函数定义如下:
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 函数执行会返回一个对象,对象结构如下:
{
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
该对象即 ref,执行 compile 函数实际调用的是 createCompiler 中 compile,而 compile 中的核心逻辑是:
var compiled = baseCompile(template$$1, finalOptions);
这里的 baseCompile 即 createCompilerCreator 传入的参数,可见真正完成 template 编译的是 baseCompile 函数。
baseCompile 源码分析
baseCompile 源码如下:
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 方法十分复杂,它的主要结构如下:
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 格式,所以解析流程是一致的。