模板DSL的编译器
1.编译器概述
编译器实际上是一段程序,他用来将一种语言A翻译为另一种语言B。其中,A被称为源代码,B被称为目标代码。编译器将源代码翻译为目标代码的过程被称为编译。完整的编译过程通常包含词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成等步骤。而整个编译过程通常分为编译前端和编译后端,其中编译前端包括词法分析、语法分析和语义分析,编译前端通常与目标平台无关,仅负分析源代码。编译后端则通常与平台有关,设计中间打吗生成、优化和目标代码生成。但是,编译后端不一定会带有中间代码和优化这两个环节,这取决于具体实现。因此,中间代码生成和优化通常也被称为“中端”。
对于Vue来说,源代码就是组件模板,目标代码就是能在浏览器平台上运行的JavaScript代码。而Vue模板编译器的目标代码实际上就是渲染函数。Vue编译模板会首先对模板进行词法分析和语法分析,得到模板AST。接着将模板AST转换为JavaScript AST。最后,根据JavaScript AST生成目标JavaScript代码,即渲染函数代码。
2.Vue.js编译器的组成
- 用来将模板字符串解析为模板 AST 的解析器(parser)
- 用来将模板 AST 转换为 JavaScript AST 的转换器 (transformer)
- 用来根据 JavaScript AST 生成渲染函数代码的生成器 (generator)。
parser的实现原理
1.有限状态自动机
parser的入参是字符串模板,解析器会逐个读取字符串模板中的字符,然后根据一定的规则将这些字符切割为一个一个的token。假设对于这样一段模板:
<p>
TEXT
</p>
它会被切割为’<p>‘、‘TEXt’和’</p>’。而这个切割的过程,则是使用有限状态机实现的。
代码实现:
现在,运行一下上面的代码,会得到下面的结果:
const res = tokenize(`<p>Text</p>`);
console.log(res);
// [
// { type: 'tag', name: 'p' },
// { type: 'text', content: 'Text' },
// { type: 'tagEnd', name: 'p' }
// ]
2.构建AST
构建AST这个过程主要是借助一个栈来实现的,首先将根节点压入栈中,当碰到起始标签,就将起始标签推入栈中。每次有新元素的时候,他的父节点都是当前栈顶元素。当碰到结束标签时,就将当前栈顶元素弹出即可。但是目前这只是最基本实现,只能生成典型模板的AST,对于一些自闭合标签和语法不完整的模板是暂时没有处理能力的。
3.AST转换
接下来就是第二步AST转换,也就是将模板AST转换为JavaScript AST。首先我们封装一个工具函数,用于打印当前节点信息:
然后我们定义一个traverseNode函数,该函数用来对模板AST进行遍历转换操作。该函数可以通过传入配置参数的方式,指定转换规则。
4.转换上下文与节点操作
我们在传入遍历AST的配置项中,再加入几项参数:当前转换节点、当前转换节点的父节点,当前结点在父节点children中的索引、节点的替换和移除方法。
5.进入与退出
可以添加一个栈,使有先进后出的函数能够按照栈的结构执行。
将模板AST转换为JavaScript AST
JavaScript AST的基本结构
<div><p>Vue</p><p>Template</p></div>
对于以上模板,它对应的JavaScript AST结构为:
const FunctionDeclNode = {
type: 'FunctionDecl', // 代表该节点是函数声明
// 函数的名称是一个标识符,标识符本身也是一个节点
id: {
type: 'Identifier',
name: 'render', // name 用来存储标识符的名称,在这里它就是渲染函数的
},
params: [], // 参数,目前渲染函数还不需要参数,所以这里是一个空数组
// 渲染函数的函数体只有一个语句,即 return 语句
body: [
{
type: 'ReturnStatement',
// 最外层的 h 函数调用
return: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'h' },
arguments: [
// 第一个参数是字符串字面量 'div'
{
type: 'StringLiteral',
value: 'div',
},
// 第二个参数是一个数组
{
type: 'ArrayExpression',
elements: [
// 数组的第一个元素是 h 函数的调用
{
type: 'CallExpression',
callee: { type: 'Identifier', name: 'h' },
arguments: [
// 该 h 函数调用的第一个参数是字符串字面量
{ type: 'StringLiteral', value: 'p' },
// 第二个参数也是一个字符串字面量
{ type: 'StringLiteral', value: 'Vue' },
],
},
// 数组的第二个元素也是 h 函数的调用
{
type: 'CallExpression',
callee: { type: 'Identifier', name: 'h' },
arguments: [
// 该 h 函数调用的第一个参数是字符串字面量
{ type: 'StringLiteral', value: 'p' },
// 第二个参数也是一个字符串字面量
{ type: 'StringLiteral', value: 'Template' },
],
},
],
},
],
},
},
],
};
以及一些辅助创建AST的工具函数:
// 用来创建 StringLiteral 节点
function createStringLiteral(value) {
return {
type: 'StringLiteral',
value,
};
}
// 用来创建 Identifier 节点
function createIdentifier(name) {
return {
type: 'Identifier',
name,
};
}
// 用来创建 ArrayExpression 节点
function createArrayExpression(elements) {
return {
type: 'ArrayExpression',
elements,
};
}
// 用来创建 CallExpression 节点
function createCallExpression(callee, arguments) {
return {
type: 'CallExpression',
callee: createIdentifier(callee),
arguments,
};
}
下面使用刚才编写好的几个JavaScript AST属性创建建函数,进行标签转换。
// 转换 Root 根节点
function transformRoot(node) {
// 将逻辑编写在退出阶段的回调函数中,保证子节点全部被处理完毕
return () => {
// 如果不是根节点,则什么都不做
if (node.type !== 'Root') {
return;
}
// node 是根节点,根节点的第一个子节点就是模板的根节点,
const vnodeJSAST = node.children[0].jsNode;
// 创建 render 函数的声明语句节点,将 vnodeJSAST 作为 render 函数的参数
node.jsNode = {
type: 'FunctionDecl',
id: { type: 'Identifier', name: 'render' },
params: [],
body: [
{
type: 'ReturnStatement',
return: vnodeJSAST,
},
],
};
};
}
const transformElement = (node, context) => {
return () => {
if (node.type !== 'Element') {
return;
}
const callExp = createCallExpression('h', [createStringLiteral(node.tag)]);
node.children.length === 1
? callExp.arguments.push(node.children[0].jsNode)
: callExp.arguments.push(createArrayExpression(node.children.map((c) => c.jsNode)));
node.jsNode = callExp;
};
};
const transformText = (node, context) => {
if (node.type !== 'Text') {
return;
}
node.jsNode = createStringLiteral(node.content);
};
目标代码生成
最后,生成目标代码的代码实现如下:
const parse = require('./parse');
const transform = require('./transform');
const { createArrayExpression, createCallExpression, createIdentifier, createStringLiteral } = './h.js';
function compile(template) {
const ast = parse(template);
transform(ast);
const code = generate(ast.jsNode);
return code;
}
function generate(node) {
const context = {
code: '',
push(code) {
context.code += code;
},
// 当前缩进的级别,初始值为 0,即没有缩进
currentIndent: 0,
// 该函数用来换行,即在代码字符串的后面追加 \n 字符,
// 另外,换行时应该保留缩进,所以我们还要追加 currentIndent * 2 个空字符
newline() {
context.code += '\n' + ` `.repeat(context.currentIndent);
},
// 用来缩进,即让 currentIndent 自增后,调用换行函数
indent() {
context.currentIndent++;
context.newline();
},
// 取消缩进,即让 currentIndent 自减后,调用换行函数
deIndent() {
context.currentIndent--;
context.newline();
},
};
genNode(node, context);
return context.code;
}
function genNode(node, context) {
switch (node.type) {
case 'FunctionDecl':
genFunctionDecl(node, context);
break;
case 'ReturnStatement':
genReturnStatement(node, context);
break;
case 'CallExpression':
genCallExpression(node, context);
break;
case 'StringLiteral':
genStringLiteral(node, context);
break;
case 'ArrayExpression':
genArrayExpression(node, context);
break;
}
}
function genFunctionDecl(node, context) {
// 从 context 对象中取出工具函数
const { push, indent, deIndent } = context;
// node.id 是一个标识符,用来描述函数的名称,即 node.id.name
push(`function ${node.id.name} `);
push(`(`);
// 调用 genNodeList 为函数的参数生成代码
genNodeList(node.params, context);
push(`) `);
push(`{`);
// 缩进
indent();
// 为函数体生成代码,这里递归地调用了 genNode 函数
node.body.forEach((n) => genNode(n, context));
// 取消缩进
deIndent();
push(`}`);
}
function genNodeList(nodes, context) {
const { push } = context;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
genNode(node, context);
if (i < nodes.length - 1) {
push(', ');
}
}
}
function genArrayExpression(node, context) {
const { push } = context;
// 追加方括号
push('[');
// 调用 genNodeList 为数组元素生成代码
genNodeList(node.elements, context);
// 补全方括号
push(']');
}
function genReturnStatement(node, context) {
const { push } = context;
// 追加 return 关键字和空格
push(`return `);
// 调用 genNode 函数递归地生成返回值代码
genNode(node.return, context);
}
function genStringLiteral(node, context) {
const { push } = context;
// 对于字符串字面量,只需要追加与 node.value 对应的字符串即可
push(`'${node.value}'`);
}
function genCallExpression(node, context) {
const { push } = context;
// 取得被调用函数名称和参数列表
const { callee, arguments: args } = node;
// 生成函数调用代码
push(`${callee.name}(`);
// 调用 genNodeList 生成参数代码
genNodeList(args, context);
// 补全括号
push(`)`);
}
const ast = parse(`<div><p>Vue</p><p>Template</p></div>`);
transform(ast);
const code = generate(ast.jsNode);
// function render() {
// return h('div', [h('p', 'Vue'), h('p', 'Template')]);
// }