生成 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. 得到模板的 ASTconst ast = parse(template)// 2. 将模板 AST 转为 JS ASTtransform(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("]");
}