Vue AST的转换实现方法讲解
loyd3 人气:0本节,我们将讨论关于AST的转换。所谓AST的转换,指的是对AST进行一系列操作,将其转换为新的AST的过程。新的AST可以是原语言或原DSL的描述,也可以是其他语言或其他DSL的描述。
例如,我们可以对模板AST进行操作,将其转换为JavaScriptAST。转换后的AST可以用于代码生成。这其实就是 Vue.js 的模板编译器将模板编译为渲染函数的过程
transform函数就是用来完成AST转换工作的
为了对AST进行转换,我们需要能访问AST 的每一个节点,这样才有机会对特定节点进行修改、替换、删除等操作。
由子AST 是树型数据结构,所以我们需要编写一个深度优先的遍历算法从而实现对AST中节点的访回。
在开始编写转换代码之前,需要一个dump工具函数,用来打印当前AST中节点的信息,如下面代码所示:
function dump(node, indent = 0) { // 节点的类型 const type = node.type // 节点的描述,如果是根节点,则没有描述 // 如果是Element类型的节点,则使用node.tag作为节点的描述 // 如果是Text类型的节点,则使用node.content作为节点的描述 const desc = node.type === 'Root'?'':node.type === 'Element'?node.tag:node.content // 打印节点的类型和描述信息 console.log(`${'-'.repeat(indent)}${type}:${desc}`) // 递归地打印子节点 if(node.children){ node.children.forEach(n=>demp(n,indent+2)) } }
dump 函数会输出怎样的结果:
const ast = parse(`<div><p>Vue</p><p>Template</p></div>`) console.log(dump(ast))
运行上面这段代码,将得到如下输出:
Root:
--Element: div
----Element: p
------Text: Vue
----Element: p
-----Text: Template
接下来,我们将着手实现对AST 中节点的访问。
代码如下所示:
function traverseNode(ast){ const currentNode = ast const children = currentNode.children if(children){ for(let i=0,i<children.length;i++){ traverseNode(children[i]) } } }
traverseNode 函数用来以深度优先的方式遍历 AST,它的实现与 dump 几乎相同
有traverseNode 函数之后,即可实现对 AST 中节点的访问。例如,可以实现一个转换功能,将AST中所有p标签转换为h1标签,如下面的代码所示:
function traverseNode(ast){ // 当前节点,ast本身就是Root节点 const currentNode = ast // 对当前节点进行操作 if(currentNode.type === 'Element' && currentNode.tag === 'p'){ currentNode.tag = 'h1' } // 如果有子节点,则递归地调用traverseNode函数进行遍历 // 递归调用 const children = currentNode.children if(children){ for(let i=0;i<children.length;i++){ traverseNode(children[i]) } } }
还可以对AST进行其他转换。例如,实现一个转换,将文本节点的内容重复两次
function traverseNode(ast){ const currentNode = ast if(currentNode.type === 'Element' && currentNode.tag === 'p'){ currentNode.tag = 'h1' } if(currentNode.type === 'Text'){ currentNode.content = currentNode.content.repeat(2) } const children = currentNode.children if(children){ for(let i=0;i<children.length;i++){ traverseNode(children[i]) } } }
不过,随着功能的不断增加,traverseNode 函数将会变得越来越“臃肿”。这时,我们很自然地想到,能否对节点的操作和访问进行解耦呢? 答案是“当然可以”,我们可以使用回调函数的机制来实现解耦,如下面代码所示
// 接收第二个参数context function traverseNode(ast, context){ const currentNode = ast // context.nodeTransforms是一个数组,其中每一个元素都是一个函数 const transforms = context.nodeTransforms for(let i = 0;i<transforms.length;i++){ // 将当前节点 currentNode 和 context 都传递给 nodeTransforms 中注册的回调函数 transforms[i](currentNode,context) } const children = currentNode.children if(children){ for(let i=0;i<children.length;i++){ traverseNode(children[i]) } } }
接着,我们把回调函数存储到context.nodeTransforns 数组中,然后遍历该数组,并逐个调用注册在其中的回调函数。最后,我们将当前节点curentNode和context对像分别作为参数传递给回调函数。
有了修改后的traverseNode函数,就可以这样使用它了
function transform(ast){ // 在transform函数内创建context对象 const context = { //注册 nodeTransforms 数组 nodeTransforms:[ transformElement, // transformElement 函数用来转换标签节点 transformText //transformText 函数用来转换文本节点 ] } //调用 traverseNode 完成转换 traverseNode(ast, context) console.log(dump(ast)) }
其中transformElement和transformText函数的实现如下
function transformElement(node){ if(node.type === 'Element' && node.tag === 'p'){ node.tag = 'h1' } } function transformElement(node){ if(node.type === 'Text'){ node.content = node.content.repeat(2) } }
可以看到,解耦之后,节点操作封装到了 transformELement和 transformText 这样的独立函数中。我们甚至可以编写任意多个类似的转换函数,只需要将它们注册到 context.nodeTransforns中即可。这样就解决了功能增加所导致的 traverseNode 函数“臃肿”的问题。
加载全部内容