Vue编译器实现代码生成方法介绍
loyd3 人气:0这里将讨论如何根据JavaScript AST生成渲染函数的代码,即代码生成。代码生成本质上是字符串拼接的艺术。需要访问JavaScript AST中的节点,为每一种类型的节点生成相符的 JavaScript代码。
本节,将实现 generate函数来完成代码生成的任务。
function compile(template) { //模板 AST const ast = parse(template) //将模板 AST转换为JavaScript AST transform(ast) // 代码生成 const code = generate(ast.jsNode) return code }
在这里,代码生成也需要上下文对象。
function generate(node){ const context = { // 存储最终生成的渲染函数代码 code: '', // 在生成代码时,通过调用 push 函数完成代码的拼接 push(code){ context.code += code } } // 该方法完成代码生成的工作 genNode(node,context) // 返回渲染函数代码 return context.code }
另外,为了让最终生成的代码具有较强的可读性,要考虑生成代码的格式,这就需要扩展context对象,为其增加用来完成换行和缩进的工具函数,如下面代码所示:
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() }, // 取消缩进 deIndent(){ context.currentIndent-- context.newline() }, } genNode(node, context) return context.code }
有了这些基础能力之后,就可以开始编写 genNode 函数来完成代码生成的工作了。代码生成的原理其实很简单,只需要匹配各种类型的 JavaScript AST节点,并调用对应的生成函数即可,如下面的代码所示:
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 } }
如果后续需要增加节点类型,只需要再genNode函数中添加相应的处理分支即可
接下来逐步完善代码生成工作,首先实现函数生命语句的代码生成,即genFunctionDecl函数,如下面的代码所示:
function genFunctionDecl(node,context){ // 从context 对象中取出工具函数 const {push, indent, deIndent} = context push(`function ${node.id.name}`) push(`(`) //调用 genNodeList 为函数的参数生成代码 genNodeList(node.params, context) push(`)`) push(`{`) indent() //为函数体生成代码,这里递归地调用了 genNode 函数 node.body.forEach(n=>genNode(n,context)) deIndent() push(`}`) }
genFunctionDecl函数用来为函数声明类型的节点生成对应的 JavaScript代码。以声明节点为例,它最终生成的代码将会是:
function render(){ ...函数体 }
genNodeList函数的实现如下:
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(',') } } }
这里要注意的一点是,每处理完一个节点,需要在生成的代码后面拼接逗号。
实际上,genArrayExpression函数就利用了这个特点来实现对数组表达式的代码生成,如下面的代码所示:
function genArrayExpression(node, context){ const {push} = context // 追加方括号 push(`[`) genNodeList(node.elements, context) // 补全方括号 push(`]`) }
对于 genFunctionDecl 函数,另外需要注意的是,由于函数体本身也是一个节点数组,所以需要遍历它并递归地调用 genNode 函数生成代码。
对于ReturnStatement和StringLiteral类型的节点来说,为它们生成代码很简单,如下所示:
function genReturnStatement(node, context){ const {push} = context push(`return `) genNode(node.return, context) } function genStringLiteral(node, context){ const {push} = context // 对于字符串字面量,只需要追加与 node.value 对应的字符串即可 genNode(`'${node.value}'`) }
最后,只剩下genCallExpression 函数了,实现如下:
function genCallExpression(node, context){ const {push} = context // 取得被调用函数名称和参数列表 const {callee, arguments: args} = node // 生成函数调用代码 push(`${callee.name}`) 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')]) }
加载全部内容