• 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 插件

path.scope 中记录着作用域相关的数据,通过 scope 可以拿到整条作用域链,包括声明的变量和对该声明的引用。

这节我们实现下 scope。

思路分析

前面我们实现了 traverse 和 path,能够遍历 AST 和对 AST 增删改了,而 scope 和 path 一样也是遍历过程中记录的信息。

能生成 scope 的 AST 叫做 block,比如 FunctionDeclaration 就是 block,因为它会生成一个新的 scope。

我们把这类节点记录下来,遍历的时候遇到 block 节点会生成新的 scope,否则拿之前的 scope。

scope 中记录着 bindings,也就是声明,每个声明会记录在哪里声明的,被哪里引用的。

遇到 block 节点,创建 scope 的时候,要遍历作用域中的所有声明(VariableDeclaraion、FunctionDeclaration),记录该 binding 到 scope 中。

记录完 bindings 之后还要再遍历一次记录引用这些 binding 的 reference。

基于这种思路我们就能实现 scope 的功能。

代码实现

首先,创建 Binding 类和 Scope 类:

class Binding {
    constructor(id, path, scope, kind) {
        this.id = id;
        this.path = path;
        this.referenced = false;
        this.referencePaths = [];
    }
}
class Scope {
    constructor(parentScope, path) {
        this.parent = parentScope;
        this.bindings = {};
        this.path = path;
    }

    registerBinding(id, path) {
        this.bindings[id] = new Binding(id, path);
    }

    getOwnBinding(id) {
        return this.bindings[id];
    }

    getBinding(id) {
        let res = this.getOwnBinding(id);
        if (res === undefined && this.parent) {
            res = this.parent.getOwnBinding(id);
        }
        return res;
    }

    hasBinding(id) {
        return !!this.getBinding(id);
    }
}

bindings 是记录作用域中的每一个声明,同时我们还可以实现 添加声明 registerBinding、查找声明 getBinding、getOwnBinding、hasBidning 的方法。

getOwnBing 是只从当前 scope 查找,而 getBinding 则是顺着作用域链向上查找。

之后我们在 path 里面定义一个 scope 的 get 的方法,当需要用到 scope 的时候才会创建,因为 scope 创建之后还要遍历查找 bindings,是比较耗时的,实现 get 可以做到用到的时候才创建。

get scope() {
    if (this.__scope) {
        return this.__scope;
    }
    const isBlock = this.isBlock();
    const parentScope = this.parentPath && this.parentPath.scope;
    return this.__scope = isBlock ? new Scope(parentScope, this) : parentScope;
}

这里的 isBlock 方法的实现就是从我们记录的数据中查找该节点是否是 block,也就是是否是函数声明这种能生成作用域的节点。

isBlock() {
    return types.visitorKeys.get(this.node.type).isBlock;
}

我们在记录节点的遍历的属性的时候,也记录了该节点是否是 block:

astDefinationsMap.set("Program", {
    visitor: ["body"],
    isBlock: true,
});
astDefinationsMap.set("FunctionDeclaration", {
    visitor: ["id", "params", "body"],
    isBlock: true,
});

这样,当遍历到 block 节点的时候,就会创建 Scope 对象,然后和当前 Scope 关联起来,形成作用域链。

scope 创建完成之后我们要扫描作用域中所有的声明,记录到 scope。这里要注意的是,因为遇到函数作用域要跳过遍历,因为它有自己独立的作用域。

path.traverse({
    VariableDeclarator: (childPath) => {
        this.registerBinding(childPath.node.id.name, childPath);
    },
    FunctionDeclaration: (childPath) => {
        childPath.skip();
        this.registerBinding(childPath.node.id.name, childPath);
    },
});

记录完 binding 之后,再扫描所有引用该 binding 的地方,也就是扫描所有的 identifier。

这里要排除声明语句里面的 identifier,那个是定义变量不是引用变量。

path.traverse({
    Identifier: (childPath) => {
        if (
            !childPath.findParent(
                (p) => p.isVariableDeclarator() || p.isFunctionDeclaration()
            )
        ) {
            const id = childPath.node.name;
            const binding = this.getBinding(id);
            if (binding) {
                binding.referenced = true;
                binding.referencePaths.push(childPath);
            }
        }
    },
});

这样,我们就实现了作用域链 path.scope,可以在 visitor 中分析作用域了。

比如删除掉未被引用的变量:

traverse(ast, {
    Program(path) {
        Object.entries(path.scope.bindings).forEach(([id, binding]) => {
            if (!binding.referenced) {
                binding.path.remove();
            }
        });
    },
    FunctionDeclaration(path) {
        Object.entries(path.scope.bindings).forEach(([id, binding]) => {
            if (!binding.referenced) {
                binding.path.remove();
            }
        });
    },
});

总结

scope 是作用域相关的信息,记录着每一个声明(binding)和对该声明的引用(reference)。

只有 block 节点需要生成 scope,所以我们会记录什么节点是 block 节点,遇到 block 节点会生成 scope,否则拿之前的。

因为 scope 会遍历 AST 来注册 binding,还是比较耗时的。我们在 path 中定义了 scope 的 get 方法,用到的时候才会创建 scope。

创建 scope 时会扫描作用域中的函数声明、变量声明,记录到 bindings 中,并且提供了 getBinding、getOwnBinding、hasBinding、registerBinding 等方法。

之后再次扫描作用域,找到所有引用这些 binding 的 identifier,记录到 reference 中。

之后我们就可以在 visitor 中分析 scope 来实现类似死代码删除等功能了。

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

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