Skip to main content

@babel/helper-environment-visitor

@babel/helper-environment-visitor 是提供当前 this 上下文访问者的实用程序包。

¥@babel/helper-environment-visitor is a utility package that provides a current this context visitor.

安装

¥Installation

npm install @babel/helper-environment-visitor

用法

¥Usage

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

¥To use the package in your Babel plugin, import the required functions from @babel/helper-environment-visitor:

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

environmentVisitor

它访问相同 this 上下文中的所有 AST 节点到根遍历节点。单独运行这个访问者是无操作的,因为它不会修改 AST 节点。此访问者旨在与 traverse.visitors.merge 一起使用。

¥It visits all AST nodes within the same this context to the root traverse node. Running this visitor alone is no-op as it does not modify AST nodes. This visitor is meant to be used with 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 的计算键和装饰器,以便在当前遍历队列耗尽后重新访问它们。有关更多用法,请参阅 example 部分。

¥Requeue the computed key and decorators of a class member path so that they will be revisited after current traversal queue is drained. See the example section for more usage.

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

示例

¥Example

替换顶层 this

¥Replace top level this

假设我们正在从 vanilla JavaScript 迁移到 ES 模块。现在 this 关键字等同于 ESModule (spec) 顶层的 undefined,我们要将所有顶层 this 替换为 globalThis

¥Suppose we are migrating from vanilla JavaScript to ES Modules. Now that the this keyword is equivalent to undefined at the top level of an ESModule (spec), we want to replace all top-level this to globalThis:

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

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

我们可以起草一个代码模组插件,这是我们的第一次修订:

¥We can draft a code mod plugin, here is our first revision:

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:例如 函数声明、对象方法和类方法:

¥The first revision works for examples so far. However, it does not really capture the idea of top-level: For example, we should not replace this within a non-arrow function: e.g. function declaration, object methods and class methods:

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

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

如果遇到这种非箭头函数,我们可以跳过遍历。在这里,我们在访问者选择器中将多个 AST 类型与 | 组合在一起。

¥We can skip traversing if we encounter such non-arrow functions. Here we combine multiple AST types with | in the visitor selector.

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

¥"FunctionDeclaration|..." is a really long string and can be difficult to maintain. We can shorten it by using the FunctionParent alias:

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 的边缘情况:

¥The plugin works generally. However, it can not handle an edge case where top-level this is used within computed class elements:

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

这是上面高亮部分的简化语法树:

¥Here is a simplified syntax tree of the highlighted section above:

{
"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 派上用场的地方:

¥If the entire ClassMethod node is skipped, then we won't be able to visit the this.foo under the key property. However, we must visit it as it could be any expression. To achieve this, we need to tell Babel to skip only the ClassMethod node, but not its computed key. This is where requeueComputedKeyAndDecorators comes in handy:

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 可以在类属性的计算键中使用:

¥There is still one missing edge case: this can be used within computed keys of a class property:

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

尽管 requeueComputedKeyAndDecorators 也可以处理这种边缘情况,但此时插件变得相当复杂,需要花费大量时间来处理 this 上下文。事实上,把重点放在处理 this 上已经偏离了实际需求,即用 globalThis 替换顶层 this

¥Although requeueComputedKeyAndDecorators can handle this edge case as well, the plugin has become quite complex at this point, with a significant amount of time spent on handling the this context. In fact, the focus on dealing with this has detracted from the actual requirement, which is to replace top-level this with globalThis.

创建 environmentVisitor 是为了简化代码,将容易出错的 this 处理逻辑提取到辅助函数中,这样你就不必再直接处理它。

¥The environmentVisitor is created to simplify the code by extracting the error-prone this-handling logic into a helper function, so that you no longer have to deal with it directly.

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 资源管理器 上试用最终修订版。

¥You can try out the final revision on the AST Explorer.

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

¥As its name implies, requeueComputedKeyAndDecorators supports ES decorators as well:

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

由于规范不断发展,使用 environmentVisitor 比实现你自己的 this 上下文访问者更容易。

¥Since the spec continues to evolve, using environmentVisitor can be easier than implementing your own this context visitor.

查找所有 super() 调用

¥Find all super() calls

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

¥This is a code snippet from @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,
]);