[Vue] Vue 模板编译原理解析 part3
生成 JavaScript AST
我们要对整个模板的 AST 进行转换,转换为 JS AST。
我们目前的代码已经有了遍历模板 AST,并且针对不同的节点,做不同操作的能力。
我们首先需要知道 JS AST 长什么样子:
function render(){
return null;
}
上面的代码,所对应的 JS AST 如下图所示:
这里有几个比较关键的部分:
- id:对应的是我函数的名称,类型为 Identifier
- params:对应的是函数的参数,是一个数组的形式来表示的
- body:对应的是函数体,由于函数体是可以有多条语句的,因此也是一个数组
我们仿造上面的设计,自己设计一个基本的数据结构来描述函数声明语句:
const FunctionDeclNode = {
type: 'FunctionDecl', // 表示该节点是一个函数声明
id: {
type: 'Identifier',
name: 'render', // 函数的名称
},
params: [],
body: [
{
type: 'ReturnStatement',
return: null
}
]
}
回到我们上面的模板:
<div><p>Vue</p><p>React</p></div>
转换出来的渲染函数:
function render(){
return h('div', [
h('p', 'Vue'),
h('p', 'React')
])
}
根据渲染函数所对应的 AST 去分析对应的节点
下面说一下几个比较重要的节点:
h 函数对应的节点:
const callExp = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'h'
}
}
字符串所对应的节点:
const Str = {
type: 'StringLiteral',
value: 'div'
}
数组对应的节点:
const Arr = {
type: 'ArrayExpression',
elements: []
}
分析完节点之后,那么我们上面的那个 render 函数所对应的 AST 就应该长下面的样子:
{
"type": "FunctionDecl",
"id": {
"type": "Identifier",
"name": "render"
},
"params": [],
"body": [
{
"type": "ReturnStatement",
"return": {
"type": "CallExpression",
"callee": {"type": "Identifier", "name": "h"},
"arguments": [
{ "type": "StringLiteral", "value": "div"},
{"type": "ArrayExpression","elements": [
{
"type": "CallExpression",
"callee": {"type": "Identifier", "name": "h"},
"arguments": [
{"type": "StringLiteral", "value": "p"},
{"type": "StringLiteral", "value": "Vue"}
]
},
{
"type": "CallExpression",
"callee": {"type": "Identifier", "name": "h"},
"arguments": [
{"type": "StringLiteral", "value": "p"},
{"type": "StringLiteral", "value": "React"}
]
}
]
}
]
}
}
]
}
分析完结构之后,下一步我们就是书写对应的转换函数。在转换函数之前,我们需要一些辅助函数,这些辅助函数用于帮助我们创建 JS AST 的节点:
function createStringLiteral(value) {
return {
type: "StringLiteral",
value,
};
}
function createIdentifier(name) {
return {
type: "Identifier",
name,
};
}
function createArrayExpression(elements) {
return {
type: "ArrayExpression",
elements,
};
}
function createCallExpression(callee, args) {
return {
type: "CallExpression",
callee: createIdentifier(callee),
arguments: args,
};
}
接下来,我们就需要去修改我们的转换函数,一个有三个转换函数,分别是:
- transformText
function transformText(node, context) {
if (node.type !== "Text") return;
node.jsNode = createStringLiteral(node.content);
}
- transformElement
// 接下来我们就可以书写一些转换函数
// 将之前写在 traverseNode 里面的各种转换逻辑抽离出来了
function transformElement(node) {
// 对外部返回一个函数,这个函数就是在退出节点时要执行的回调函数
return () => {
if (node.type !== "Element") return;
// 1. 创建 h 函数的 AST 节点
const callExp = createCallExpression("h", [
createStringLiteral(node.tag),
]);
// 2. 处理 h 函数里面的参数
node.children.length === 1
? // 如果之后一个子节点,那么直接将子节点的 jsNode 作为参数即可
callExp.arguments.push(node.children[0].jsNode)
: // 如果是多个子节点,那么就需要将子节点的 jsNode 作为数组传入
callExp.arguments.push(
createArrayExpression(
node.children.map((child) => child.jsNode)
)
);
};
}
- transformRoot
// 最后再写一个转换函数,负责转换 Root 根节点
function transformRoot(node) {
return () => {
if (node.type !== "Root") return;
// 生成最外层的节点
const vnodeJSAST = node.children[0].jsNode;
node.jsNode = {
type: "FunctionDecl",
id: {
type: "Identifier",
name: "render",
},
params: [],
body: [
{
type: "ReturnStatement",
return: vnodeJSAST,
},
],
};
};
}
最后在 transform 中使用这三个转换函数
生成器
整理一下思绪,哪怕你前面都没有听懂,但是你需要知道我们走到哪一步了。
目前我们已经有 js ast,只剩下最后一步,革命就成功了。
遍历这个生成的 js ast,转为具体的渲染函数
function compile(template){
// 1. 得到模板的 AST
const ast = parse(template)
// 2. 将模板 AST 转为 JS AST
transform(ast)
// 3. 代码生成
const code = genrate(ast.jsNode);
return code;
}
和转换器一样,我们在生成器内部也需要维护一个上下文对象,为我们提供一些辅助函数和必要的信息:
// 和上一步转换器非常相似,我们也需要一个上下文对象
const context = {
// 存储最终所生成的代码
code: "",
// 在生成代码的时候,通过调用 push 方法来进行拼接
push(code) {
context.code += code;
},
// 当前缩进的级别,初始值为 0,也就是没有缩进
currentIndent: 0,
// 该方法用来换行,会根据当前缩进的级别来添加相应的缩进
newline() {
context.code += "\n" + ` `.repeat(context.currentIndent);
},
// 用来缩进,会将缩进级别加一
indent() {
context.currentIndent++;
context.newline();
},
// 用来取消缩进,会将缩进级别减一
deIndent() {
context.currentIndent--;
context.newline();
},
};
之后调用 genNode 方法,而 genNode 方法的内部,就是根据不同的 AST 节点类型,调用对应的生成方法:
function genNode(node, context) {
// 我这里要做的事情,就是根据你当前节点的 type 来调用不同的方法
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) {
// 从上下文中获取一些实用函数
const { push, indent, deIndent } = context;
// 向输出中添加 "function 函数名"
push(`function ${node.id.name} `);
// 添加左括号开始参数列表
push(`(`);
// 生成参数列表
genNodeList(node.params, context);
// 添加右括号结束参数列表
push(`) `);
// 添加左花括号开始函数体
push(`{`);
// 缩进,为函数体的代码生成做准备
indent();
// 遍历函数体中的每个节点,生成相应的代码
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(`, `);
}
}
}
// 生成 return 语句
function genReturnStatement(node, context) {
const { push } = context;
// 添加 "return "
push(`return `);
// 生成 return 语句后面的代码
genNode(node.return, context);
}
// 生成函数调用表达式
function genCallExpression(node, context) {
const { push } = context;
const { callee, arguments: args } = node;
// 添加 "函数名("
push(`${callee.name}(`);
// 生成参数列表
genNodeList(args, context);
// 添加 ")"
push(`)`);
}
// 生成字符串字面量
function genStringLiteral(node, context) {
const { push } = context;
// 添加 "'字符串值'"
push(`'${node.value}'`);
}
// 生成数组表达式
function genArrayExpression(node, context) {
const { push } = context;
// 添加 "["
push("[");
// 生成数组元素
genNodeList(node.elements, context);
// 添加 "]"
push("]");
}