跳至主内容

@babel/helper-environment-visitor

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

@babel/helper-environment-visitor 是一个提供当前 this 上下文访问器的工具包。

安装

npm install @babel/helper-environment-visitor

用法

要在 Babel 插件中使用此包,请从 @babel/helper-environment-visitor 导入所需函数:

my-babel-plugin.js
import environmentVisitor, {
requeueComputedKeyAndDecorators
} from "@babel/helper-environment-visitor";

environmentVisitor

该访问器会遍历根遍历节点内相同 this 上下文中的所有 AST 节点。单独运行此访问器是无操作(no-op),因为它不会修改 AST 节点。此访问器设计用于与 traverse.visitors.merge 配合使用。

collect-await-expression.plugin.js
module.exports = (api) => {
const { types: t, traverse } = api;
return {
name: "collect-await",
visitor: {
Function(path) {
if (path.node.async) {
const awaitExpressions = [];
// Get a list of related await expressions within the async function body
path.traverse(traverse.visitors.merge([
environmentVisitor,
{
AwaitExpression(path) {
awaitExpressions.push(path);
},
ArrowFunctionExpression(path) {
path.skip();
},
}
]))
}
}
}
}
}

requeueComputedKeyAndDecorators

requeueComputedKeyAndDecorators(path: NodePath): void

将类成员 path 的计算键和装饰器重新排队,使其在当前遍历队列清空后被重新访问。更多用法请参见示例部分。

my-babel-plugin.js
if (path.isMethod()) {
requeueComputedKeyAndDecorators(path)
}

示例

替换顶层的 this

假设我们正在从原生 JavaScript 迁移到 ES 模块。由于在 ES 模块顶层中,this 关键字等同于 undefined规范),我们需要将所有顶层的 this 替换为 globalThis

input.js
// replace this expression to `globalThis.foo = "top"`
this.foo = "top";

() => {
// replace
this.foo = "top"
}

我们可以编写一个代码改造插件,以下是初始版本:

Revision 1: replace-top-level-this-plugin.js
module.exports = (api) => {
const { types: t } = api;
return {
name: "replace-top-level-this",
visitor: {
ThisExpression(path) {
path.replaceWith(t.identifier("globalThis"));
}
}
}
}

当前版本在简单情况下有效,但未能准确捕获"顶层"概念:例如,我们不应在非箭头函数(如函数声明、对象方法和类方法)中替换 this

input.js
function Foo() {
// don't replace
this.foo = "inner";
}

class Bar {
method() {
// don't replace
this.foo = "inner";
}
}

遇到这类非箭头函数时,我们可以跳过遍历。这里我们在访问器选择器中使用 | 组合多个 AST 类型:

Revision 2: replace-top-level-this-plugin.js
module.exports = (api) => {
const { types: t } = api;
return {
name: "replace-top-level-this",
visitor: {
ThisExpression(path) {
path.replaceWith(t.identifier("globalThis"));
}
"FunctionDeclaration|FunctionExpression|ObjectMethod|ClassMethod|ClassPrivateMethod"(path) {
path.skip();
}
}
}
}

"FunctionDeclaration|..." 这样的字符串过长且难以维护。我们可以使用 FunctionParent 别名来简化:

Revision 3: replace-top-level-this-plugin.js
module.exports = (api) => {
const { types: t } = api;
return {
name: "replace-top-level-this",
visitor: {
ThisExpression(path) {
path.replaceWith(t.identifier("globalThis"));
}
FunctionParent(path) {
if (!path.isArrowFunctionExpression()) {
path.skip();
}
}
}
}
}

插件基本可用,但无法处理在计算类元素中使用顶层 this 的边缘情况:

input.js
class Bar {
// replace
[this.foo = "outer"]() {
// don't replace
this.foo = "inner";
}
}

以下是上述高亮部分的简化语法树:

{
"type": "ClassMethod", // skipped
"key": { "type": "AssignmentExpression" }, // [this.foo = "outer"]
"body": { "type": "BlockStatement" }, // { this.foo = "inner"; }
"params": [], // should visit too if there are any
"computed": true
}

如果跳过整个 ClassMethod 节点,我们将无法访问 key 属性下的 this.foo。但由于它可能是任何表达式,我们必须访问它。为此,需要让 Babel 仅跳过 ClassMethod 节点而非其计算键。这正是 requeueComputedKeyAndDecorators 的用途:

Revision 4: replace-top-level-this-plugin.js
import {
requeueComputedKeyAndDecorators
} from "@babel/helper-environment-visitor";

module.exports = (api) => {
const { types: t } = api;
return {
name: "replace-top-level-this",
visitor: {
ThisExpression(path) {
path.replaceWith(t.identifier("globalThis"));
}
FunctionParent(path) {
if (!path.isArrowFunctionExpression()) {
path.skip();
}
if (path.isMethod()) {
requeueComputedKeyAndDecorators(path);
}
}
}
}
}

仍有一个遗漏的边缘情况:this 可能在类属性的计算键中使用:

input.js
class Bar {
// replace
[this.foo = "outer"] =
// don't replace
this.foo
}

虽然 requeueComputedKeyAndDecorators 也能处理此情况,但此时插件已变得相当复杂,大量时间花费在处理 this 上下文上。实际上,对 this 处理的关注已偏离了核心需求——用 globalThis 替换顶层的 this

environmentVisitor 的创建正是为了将易错的 this 处理逻辑提取到辅助函数中,从而简化代码,使开发者无需直接处理这些细节。

Revision 5: replace-top-level-this-plugin.js
import environmentVisitor from "@babel/helper-environment-visitor";

module.exports = (api) => {
const { types: t, traverse } = api;
return {
name: "replace-top-level-this",
visitor: traverse.visitors.merge([
{
ThisExpression(path) {
path.replaceWith(t.identifier("globalThis"));
}
},
environmentVisitor
]);
}
}

你可以在 AST Explorer 上尝试最终版本。

顾名思义,requeueComputedKeyAndDecorators 也支持 ES 装饰器

input.js
class Foo {
// replaced to `@globalThis.log`
@(this.log) foo = 1;
}

由于规范仍在持续演进,使用 environmentVisitor 比自己实现针对 this 上下文的访问者更为便捷。

查找所有 super() 调用

这是来自 @babel/helper-create-class-features-plugin代码片段

src/misc.ts
const findBareSupers = traverse.visitors.merge<NodePath<t.CallExpression>[]>([
{
Super(path) {
const { node, parentPath } = path;
if (parentPath.isCallExpression({ callee: node })) {
this.push(parentPath);
}
},
},
environmentVisitor,
]);