traverse组件用来遍历AST,简单说就是把AST上的各个节点都运行一遍,但单纯把节点都运行一遍是没有意义的,所以traverse需要配合visitor使用。
    visitor是一个对象,它可以定义一些方法,用来过滤节点。接下来用一个实际案来解traverse和visitor的效果,代码如下;

    1. const fs = require('fs');
    2. const parser = require("@babel/parser");
    3. const traverse = require("@babel/traverse").default;
    4. const t = require("@babel/types");
    5. const generator = require("@babel/generator").default;
    6. const jscode = fs.readFileSync("./demo.js", {
    7. encoding: "utf-8"
    8. });
    9. let ast = parser.parse(jscode);
    10. let visitor = {};
    11. visitor.FunctionExpression = function(path){
    12. console.log("javaScriptAST");
    13. };
    14. traverse(ast, visitor);
    15. let code = generator(ast).code;
    16. fs.writeFile('./demoNew.js', code, (err) => {});
    17. // 输出 javaScriptAST

    首先声明对象,对象的名字可随意定义,再给对象增加一个名为FunctionExpression的
    方法,它的名字是需要遍历的节点类型,需要注意大小写。traverse会遍历所有的节点,当节点类型为FunctionExpression时,调用visitor中相应的方法。如果想要处理其他节点类型,例如Identifier,可以在visitor中继续定义方法,以Identifier命名即可。visitor中的方法接收一个参数,traverse在遍历时,会把当前节点的Path对象传给它,传过来的是Path对象而非节点(Node)。最后把visitor作为第二个参数传到traverse里,传给traverse的第一个参数是整个AST。这段代码的意思是,从头开始遍历AST中的所有节点,过滤出
    FunctionExpression节点,执行相应的方法。在原始代码中,有两个FunctionExpression节点,因此,会输出两次 javaScriptAST。
    定义visitor的方式有以下三种,最常用的是visitor2这种形式。

    1. const visitor1 = {
    2. FunctionExpression: function (path) {
    3. console.log("javaScriptAST");
    4. }
    5. };
    6. const visitor2 = {
    7. FunctionExpression (path) {
    8. console.log("javaScriptAST");
    9. }
    10. };
    11. const visitor3 = {
    12. FunctionExpression: {
    13. enter(path) {
    14. console.log("javaScriptAST");
    15. }
    16. }
    17. };

    在visitor3中,存在一个enter。在遍历节点的过程中,有两次机会来访问一个节点,即进入节点时(enter)与退出节点时(exit)。以原始代码中的add函数为例,节点的遍历过程可描述如下:

    1. 进入FunctionExpression
    2. 进入Identifier(params[0])走到尽头
    3. 退出Identifier(params[0])
    4. 进入Identifier(params[1])走到尽头
    5. 退出Identifier(params[1])
    6. 进入BlockStatement(body)
    7. 进入ReturnStatement(body)
    8. 进入BinaryExpression(argument)
    9. 进入BinaryExpression(left)
    10. 进入Identifier(left)走到尽头
    11. 退出Identifier(left)
    12. 进人Identifier(right)走到尽头
    13. 退出Identifier(right)
    14. 退出BinaryExpression(left)
    15. 进入NumericLiteral(right)走到尽头
    16. 退出NumericLiteral(right)
    17. 退出BinaryExpression(argument)
    18. 退出ReturnStatement(body)
    19. 退出BlockStatement(body)
    20. 退出FunctionExpression

    正确选择节点处理时机,有助于提高代码效率。可以看出traverse是一个深度优先的遍历过程。因此,如果存在父子节点,那么enter的处理时机是先处理父节点,再处理子节点。而exit的处理时机是先处理子节点,再处理父节点。traverse默认是在enterI时处理,
    如果要在exit时处理,必须在visitor中写明。

    1. const fs = require('fs');
    2. const parser = require("@babel/parser");
    3. const traverse = require("@babel/traverse").default;
    4. const t = require("@babel/types");
    5. const generator = require("@babel/generator").default;
    6. const jscode = fs.readFileSync("./demo.js", {
    7. encoding: "utf-8"
    8. });
    9. let ast = parser.parse(jscode);
    10. const visitor3 = {
    11. FunctionExpression: {
    12. enter(path) {
    13. console.log("javaScriptAST enter");
    14. },
    15. exit(path) {
    16. console.log("javaScriptAST exit");
    17. }
    18. }
    19. };
    20. traverse(ast, visitor3);
    21. let code = generator(ast).code;
    22. fs.writeFile('./demoNew.js', code, (err) => {});

    还可以把方法名用 “|” 连接成FunctionExpression | BinaryExpression 形式的字符串,把同一个函数应用到多个节点,例如:

    1. const fs = require('fs');
    2. const parser = require("@babel/parser");
    3. const traverse = require("@babel/traverse").default;
    4. const t = require("@babel/types");
    5. const generator = require("@babel/generator").default;
    6. const jscode = fs.readFileSync("./demo.js", {
    7. encoding: "utf-8"
    8. });
    9. let ast = parser.parse(jscode);
    10. const visitor = {
    11. "FunctionExpression|BinaryExpression"(path){
    12. console.log("javaScriptAST");
    13. }
    14. };
    15. traverse(ast, visitor);
    16. let code = generator(ast).code;
    17. fs.writeFile('./demoNew.js', code, (err) => {});

    也可以把多个函数应用于同一个节点。原先是把一个函数赋值给enter或者exit,现在改为函数的数组,会按顺序依次执行。示例代码如下:

    1. const fs = require('fs');
    2. const parser = require("@babel/parser");
    3. const traverse = require("@babel/traverse").default;
    4. const t = require("@babel/types");
    5. const generator = require("@babel/generator").default;
    6. const jscode = fs.readFileSync("./demo.js", {
    7. encoding: "utf-8"
    8. });
    9. let ast = parser.parse(jscode);
    10. function func1(path){
    11. console.log('func1');
    12. }
    13. function func2(path){
    14. console.log('func2');
    15. }
    16. const visitor = {
    17. FunctionExpression: {
    18. enter: [func1, func2]
    19. }
    20. };
    21. traverse(ast, visitor);
    22. let code = generator(ast).code;
    23. fs.writeFile('./demoNew.js', code, (err) => {});

    traverse并非必须从头遍历,它可在任意节点向下遍历。例如,想要把代码中所有函数的第一个参数改为x,代码如下:

    1. const fs = require('fs');
    2. const parser = require("@babel/parser");
    3. const traverse = require("@babel/traverse").default;
    4. const t = require("@babel/types");
    5. const generator = require("@babel/generator").default;
    6. const jscode = fs.readFileSync("./demo.js", {
    7. encoding: "utf-8"
    8. });
    9. let ast = parser.parse(jscode);
    10. const updateParamNameVisitor = {
    11. Identifier(path) {
    12. if (path.node.name === this.paramName) {
    13. path.node.name = "x";
    14. }
    15. }
    16. };
    17. const visitor = {
    18. FunctionExpression(path) {
    19. const paramName = path.node.params[0].name;
    20. path.traverse(updateParamNameVisitor, {
    21. paramName
    22. });
    23. }
    24. };
    25. traverse(ast, visitor);
    26. let code = generator(ast).code;
    27. fs.writeFile('./demoNew.js', code, (err) => {});

    处理后的输出结果
    image.png
    这段代码先用traverse根据visitor去遍历所有节点。当得到 FunctionExpression 节点时,用 path.traverse 根据 updateParamNameVisitor 去遍历当前节点下的所有子节点,然后修改与函数第一个参数相同的标识符。在使用path.traverse时,还可以额外传入一个对象,在对应的visitor中用this去引用它。其中 path.node 才是当前节点,所以path.node.params[0].name可以取出函数的第一个参数名。