跳至主要內容

@babel/helper-environment-visitor

@babel/helper-environment-visitor 是提供目前 this context 訪客的實用程式套件。

安裝

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 context 中的所有 AST 節點,前往根部 traverse 節點。單獨執行這個訪客並無作用,因為它不會修改 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 的計算式金鑰和裝飾器,以便在目前的 traversal 排程清空後重新拜訪它們。請參閱 範例 區段,取得更多用法說明。

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

範例

取代頂層 this

假設我們正在從傳統 JavaScript 遷移到 ES 模組。現在,this 關鍵字等於 ES 模組頂層的 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";
}
}

如果我們遇到此類非箭頭函式,我們可以略過遍歷。這裡我們在 visitor 選擇器中使用 | 結合多個 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,
]);