• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍

    • 1.Babel 的介绍
    • 2.Babel 的编译流程
    • 3.Babel 的 AST
    • 4.Babel 的 API
    • 5.实战案例:插入函数调用参数
    • 6.JS Parser 的历史
    • 7.traverse 的 path、scope、visitor
    • 8.Generator 和 SourceMap 的奥秘
    • 9.Code- Frame 和代码高亮原理
    • 10.Babel 插件和 preset
    • 11.Babel 插件的单元测试
    • 12.Babel 的内置功能(上)
    • 13.Babel 的内置功能(下)
    • 14.Babel 配置的原理
    • 15.工具介绍:VSCode Debugger 的使用
    • 16.实战案例:自动埋点
    • 17.实战案例: 自动国际化
    • 18.实战案例:自动生成 API 文档
    • 19.实战案例: Linter
    • 20.实战案例: 类型检查
    • 21.实战案例: 压缩混淆
    • 22.实战案例: JS 解释器
    • 23.实战案例: 模块遍历
    • 24.Babel Macros
    • 25.如何调试 Babel 源码?
    • 26.手写 Babel:思路篇
    • 27.手写 Babel: parser 篇
    • 28.手写 Babel: traverse 篇
    • 29.手写 Babel: traverse -- path篇
    • 30.手写 Babel: traverse -- scope篇
    • 31.手写 Babel: generator篇
    • 32.手写 Babel: core篇
    • 33.手写 Babel: cli篇
    • 34.手写 Babel: 总结
    • 35.小册总结
    • 36.加餐:会了 babel 插件,就会写 prettier 插件

traverse 是遍历 AST,并且遍历的过程中支持 visitor 的调用,在 visitor 里实现对 AST 的增删改。

我们这一节的目的是实现这样的 api:

traverse(ast, {
    Identifier(node) {
        node.name = "b";
    },
});

path 放到下一节实现。

思路分析

AST 的遍历就是树的遍历,树的遍历就深度优先、广度优先两种方式,而这里明显是深度优先遍历。

深度优先遍历要递归的遍历节点的子节点,那么我们怎么知道对象的属性是可以遍历的子节点呢?

可以维护一份数据来保存不同 AST 的什么属性是可以遍历的,然后在遍历不同节点的时候从中查找应该继续遍历什么属性。这样就实现了深度优先遍历。

在遍历的过程中可以根据类型调用不同的 visitor,然后传入当前节点。

代码实现

首先,我们维护这样一份数据:不同的 AST 有哪些可以遍历的属性。

比如 Program 要遍历 body 属性,VariableDeclarator 要遍历 id、init 属性等:

const astDefinationsMap = new Map();

astDefinationsMap.set("Program", {
    visitor: ["body"],
});
astDefinationsMap.set("VariableDeclaration", {
    visitor: ["declarations"],
});
astDefinationsMap.set("VariableDeclarator", {
    visitor: ["id", "init"],
});
astDefinationsMap.set("Identifier", {});
astDefinationsMap.set("NumericLiteral", {});
astDefinationsMap.set("FunctionDeclaration", {
    visitor: ["id", "params", "body"],
});
astDefinationsMap.set("BlockStatement", {
    visitor: ["body"],
});
astDefinationsMap.set("ReturnStatement", {
    visitor: ["argument"],
});
astDefinationsMap.set("BinaryExpression", {
    visitor: ["left", "right"],
});
astDefinationsMap.set("ExpressionStatement", {
    visitor: ["expression"],
});
astDefinationsMap.set("CallExpression", {
    visitor: ["callee", "arguments"],
});

然后实现递归的遍历,遍历到不同节点时,取出不同节点要遍历的属性,然后递归遍历。如果是数组的话,每个元素都是这样处理:

function traverse(node, visitors) {
    const defination = astDefinationsMap.get(node.type);

    if (defination.visitor) {
        defination.visitor.forEach((key) => {
            const prop = node[key];
            if (Array.isArray(prop)) {
                // 如果该属性是数组
                prop.forEach((childNode) => {
                    traverse(childNode, visitors);
                });
            } else {
                traverse(prop, visitors);
            }
        });
    }
}

实现了遍历,当然还要在遍历时支持不同节点的 visitor 回调函数:

visitor 支持 enter 和 exit 阶段,也就是进入节点调用 enter 回调函数,之后遍历子节点,之后再调用 exit 回调函数。

那么分别在遍历前后调用就可以,默认如果没有指定哪个阶段就在 enter 阶段调用。

function traverse(node, visitors) {
    const defination = astDefinationsMap.get(node.type);

    let visitorFuncs = visitors[node.type] || {};

    if (typeof visitorFuncs === "function") {
        visitorFuncs = {
            enter: visitorFuncs,
        };
    }

    visitorFuncs.enter && visitorFuncs.enter(node);

    if (defination.visitor) {
        defination.visitor.forEach((key) => {
            const prop = node[key];
            if (Array.isArray(prop)) {
                // 如果该属性是数组
                prop.forEach((childNode) => {
                    traverse(childNode, visitors);
                });
            } else {
                traverse(prop, visitors);
            }
        });
    }
    visitorFuncs.exit && visitorFuncs.exit(node);
}

这样,我们就实现了 AST 的遍历和 enter、exit 阶段的 visitor 调用。

为什么要分 enter 和 exit 两个阶段呢?

因为 enter 阶段在遍历子节点之前,那么修改之后就可以立刻遍历子节点,而 exit 是在遍历结束之后了,所以不会继续遍历子节点。如果 enter 阶段修改了 AST 但是不想遍历新生成的子节点,可以用 path.skip 跳过遍历。

可以这样来遍历和修改 AST:

traverse(ast, {
    Identifier(node) {
        node.name = "b";
    },
});

总结

traverse 就是 AST 的遍历,而树的遍历就深度优先和广度优先两种,这里是深度优先,我们维护了一份什么 AST 遍历什么属性的数据,然后遍历的时候就可以知道如何遍历每一个节点。

遍历的时候调用 visitor 的回调函数,分为 enter 和 exit 阶段来调用,默认是 enter 阶段。

(代码在这里,建议 git clone 下来通过 node 跑一下)

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
27.手写 Babel: parser 篇
Next
29.手写 Babel: traverse -- path篇