跳转到主要内容

@babel/helper-environment-visitor

@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 节点。单独运行此访问器是无操作的,因为它不会修改 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 模块。现在 this 关键字在 ESModule 的顶层等效于 undefined规范),我们希望将所有顶级 this 替换为globalThis

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

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

我们可以起草一个代码修改插件,这是我们的第一个版本

版本 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 类型。

版本 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 别名来缩短它

版本 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 派上用场的地方

版本 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 处理逻辑提取到辅助函数中来简化代码,这样您就不必直接处理它了。

版本 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,
]);