# tapable 中文文档

规范地址：https://npmcn.com/package/tapable/

版本：2\.3\.3
npm 元数据更新时间：2026-04-21T13:37:30.534Z
中文译文更新时间：2026-11-05T01:23:46.658Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/tapable
原文对照：https://npmcn.com/package/tapable/?lang=original
项目主页：https://github.com/webpack/tapable
源码仓库：https://github.com/webpack/tapable

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：73.4 kB，20 个文件
- 模块格式：CommonJS
- TypeScript 类型：包内自带
- Node.js 要求：\>=6
- 运行依赖：0 个
- 首次发布：2013-01-21T11:27:02.964Z
- 最新版本发布：2026-04-21T13:37:30.215Z
- 近 30 天下载量：626296851（2026-10-07 至 2026-11-05，npm 官方下载统计）
- 已发布版本数：54（近 12 个月 3 个）

## 中文简介

只是一个插件的小模块。

## 安装

```sh
npm install tapable
```

也可以使用：`pnpm add tapable`、`yarn add tapable`、`bun add tapable`

## README

# Tapable

tapable包公开了许多Hook类，这些类可用于为插件创建钩子。

```javascript
const {
    AsyncParallelBailHook,
    AsyncParallelHook,
    AsyncSeriesBailHook,
    AsyncSeriesHook,
    AsyncSeriesWaterfallHook,
    SyncBailHook,
    SyncHook,
    SyncLoopHook,
    SyncWaterfallHook
} = require("tapable");
```

## 安装

```shell
npm install --save tapable
```

## 用法

所有Hook构造函数都有一个可选参数，它是一个字符串形式的参数名称列表。

```js
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
```

最佳实践是在类中公开类的所有钩子 `hooks` 属性:

```js
class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncHook(["newSpeed"]),
            brake: new SyncHook(),
            calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
        };
    }

    /* ... */
}
```

其他人现在可以使用这些钩子：

```js
const myCar = new Car();

// Use the tap method to add a consumer (plugin)
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
```

它需要传递一个名称来标识插件/原因。

您可能会收到以下参数：

```js
myCar.hooks.accelerate.tap("LoggerPlugin", (newSpeed) =>
    console.log(`Accelerating to ${newSpeed}`)
);
```

对于同步钩子， `tap` 是添加插件的唯一有效方法。异步钩子也支持异步插件：

```js
myCar.hooks.calculateRoutes.tapPromise(
    "GoogleMapsPlugin",
    (source, target, routesList) =>
        // return a promise
        google.maps.findRoute(source, target).then((route) => {
            routesList.add(route);
        })
);
myCar.hooks.calculateRoutes.tapAsync(
    "BingMapsPlugin",
    (source, target, routesList, callback) => {
        bing.findRoute(source, target, (err, route) => {
            if (err) return callback(err);
            routesList.add(route);
            // call the callback
            callback();
        });
    }
);

// You can still use sync plugins
myCar.hooks.calculateRoutes.tap(
    "CachedRoutesPlugin",
    (source, target, routesList) => {
        const cachedRoute = cache.get(source, target);
        if (cachedRoute) routesList.add(cachedRoute);
    }
);
```

声明这些钩子的类需要调用它们：

```js
class Car {
    /**
     * You won't get returned value from SyncHook or AsyncParallelHook,
     * to do that, use SyncWaterfallHook and AsyncSeriesWaterfallHook respectively
     */

    setSpeed(newSpeed) {
        // following call returns undefined even when you returned values
        this.hooks.accelerate.call(newSpeed);
    }

    useNavigationSystemPromise(source, target) {
        const routesList = new List();
        return this.hooks.calculateRoutes
            .promise(source, target, routesList)
            .then((res) =>
                // res is undefined for AsyncParallelHook
                routesList.getRoutes()
            );
    }

    useNavigationSystemAsync(source, target, callback) {
        const routesList = new List();
        this.hooks.calculateRoutes.callAsync(source, target, routesList, (err) => {
            if (err) return callback(err);
            callback(null, routesList.getRoutes());
        });
    }
}
```

钩子将编译一个方法，以最有效的方式运行你的插件。它生成的代码取决于：

-   已注册插件的数量（无，一个，多个）
-   注册的插件类型（sync, async, promise）
-   使用的调用方法（sync, async, promise）
-   参数的数量
-   是否使用拦截

这确保了最快的执行速度。看到 [代码生成](https://npmcn.com/package/tapable/#readme-code-generation) 有关运行时编译的详细信息。

## 插件API

在钩子上注册一个回调函数 `tap*` 方法。钩子类型决定哪些是有效的(参见 [钩类](https://npmcn.com/package/tapable/#readme-hook-classes)）：

-   `hook.tap(nameOrOptions, fn)` -注册一个同步回调。
-   `hook.tapAsync(nameOrOptions, fn)` -注册一个基于回调的异步回调。传递给的最后一个参数 `fn` 是节点风格的回调吗 `(err, result)`。
-   `hook.tapPromise(nameOrOptions, fn)` -注册一个返回承诺的异步回调。如果 `fn` 返回一些不可用的东西，钩子抛出。

第一个参数可以是一个字符串（插件名）或一个选项对象，也允许影响点击运行的顺序：

```js
hook.tap(
    {
        name: "MyPlugin",
        stage: -10, // lower stages run earlier, default is 0
        before: "OtherPlugin" // run before a named tap (string or string[])
    },
    (...args) => {
        /* ... */
    }
);
```

| 选项 | 类型 | 描述 |
| --- | --- | --- |
| `name` | `string` | 必需的。标识用于调试、拦截器和 `before` 选择。 |
| `stage` | `number` | 默认为 `0`． 低级水龙头先于高级水龙头运行。具有相同阶段的抽头按注册顺序运行。 |
| `before` | `string` | `string[]` | 该抽头插入在指定抽头之前。未知的名字将被忽略。结合 `stage`， `before` 获胜是因为它的目标用户；其他水龙头仍按 `stage`。 |

这个 `name` 也被一些生态系统（如webpack）用于分析和错误消息。在单个监听注册中，以后的拦截器 `register` 钩子仍然可以替换tap对象(参见 [拦截](https://npmcn.com/package/tapable/#readme-interception)）.

### `hook.withOptions(options)`

`withOptions` 返回钩子周围的facade `tap*` 方法自动合并 `options` 进入每一个注册。对于想要预配置 `stage` 或 `before` 对于他们添加的所有水龙头：

```js
const lateHook = myCar.hooks.accelerate.withOptions({ stage: 10 });
lateHook.tap("LogAfterOthers", (speed) => console.log("final speed", speed));
// equivalent to: myCar.hooks.accelerate.tap({ name: "LogAfterOthers", stage: 10 }, ...)
```

返回的对象不公开 `call*` 方法，因此分发给插件是安全的。

一个可运行的示例显示如何 `withOptions` 影响水龙头排序：

```js
const { SyncHook } = require("tapable");

const hook = new SyncHook(["value"]);

hook.tap("Default", (v) => console.log("default", v));

// Pre-configure stage: 10 so all taps registered through `late` run last.
const late = hook.withOptions({ stage: 10 });
late.tap("RunLast", (v) => console.log("last", v));

// Pre-configure stage: -10 so these taps run first. Each facade can also
// be further narrowed via `withOptions`.
const early = hook.withOptions({ stage: -10 });
early.tap("RunFirst", (v) => console.log("first", v));

hook.call(1);
// first 1
// default 1
// last 1
```

的值 `withOptions`． 例如, `late.tap({ name: "Override", stage: 0 }, fn)` 忽略表面的 `stage: 10` 和寄存器 `fn` 在阶段 `0`。

## 钩类型

每个钩子都可以有一个或几个功能。它们的执行方式取决于钩子类型：

-   基本钩（没有“瀑布”，“保释”或“循环”的名称）。这个钩子只是调用它在一行中点击的每个函数。
    
-   **瀑布**． 瀑布钩子也会调用一行中的每个被选中的函数。与基本钩子不同，它将每个函数的返回值传递给下一个函数。
    
-   **保释**． 保释挂钩允许提前退出。当任何选中的函数返回任何结果时，保释钩子将停止执行剩余的函数。
    
-   **循环**． 当一个在循环钩子中的插件返回一个未定义的值时，钩子将从第一个插件重新启动。它将循环直到所有插件返回undefined。
    

此外，钩子可以是同步的，也可以是异步的。为了反映这一点，有“Sync”，“AsyncSeries”和“AsyncParallel”钩子类：

-   **同步**． 同步钩子只能通过同步函数调用(使用 `myHook.tap()`）.
    
-   **AsyncSeries**． 异步系列钩子可以使用同步的、基于回调的和基于承诺的函数(使用 `myHook.tap()`， `myHook.tapAsync()` 和 `myHook.tapPromise()`）. 它们在一行中调用每个异步方法。
    
-   **AsyncParallel**． 异步并行钩子也可以使用同步的、基于回调的和基于承诺的函数(使用 `myHook.tap()`， `myHook.tapAsync()` 和 `myHook.tapPromise()`）. 但是，它们并行地运行每个异步方法。
    

钩子类型反映在它的类名中。例如, `AsyncSeriesWaterfallHook` 允许异步函数并串联运行它们，将每个函数的返回值传递给下一个函数。

## 钩类

下表总结了9个内置钩子类。对于每个类：

-   **利用方法** 是 `tapX` 可用于注册处理程序的变体。
-   **调用方法** 是钩子的所有者触发它的方式。
-   **结果** 返回值吗 `call` (或传递给 `callAsync` 的回调/解析 `promise` 调用)。
-   **从抽头返回值** 描述从抽头函数返回的值是否有效果。

| 类 | 利用方法 | 调用方法 | 结果 | 从抽头返回值 |
| --- | --- | --- | --- | --- |
| `SyncHook` | `tap` | `call` | `undefined` | 忽略了 |
| `SyncBailHook` | `tap` | `call` | 第一个非`undefined` 价值,或者 `undefined` | 使钩子短路 |
| `SyncWaterfallHook` | `tap` | `call` | 最终值（最后一次点击后的第一个参数） | 作为第一个参数传递给下一个点击 |
| `SyncLoopHook` | `tap` | `call` | `undefined` | 非`undefined` 从第一次点击开始重新启动循环 |
| `AsyncParallelHook` | `tap`， `tapAsync`， `tapPromise` | `callAsync`， `promise` | `undefined` | 忽略了 |
| `AsyncParallelBailHook` | `tap`， `tapAsync`， `tapPromise` | `callAsync`， `promise` | 第一个非`undefined` 价值,或者 `undefined` | 使钩子短路 |
| `AsyncSeriesHook` | `tap`， `tapAsync`， `tapPromise` | `callAsync`， `promise` | `undefined` | 忽略了 |
| `AsyncSeriesBailHook` | `tap`， `tapAsync`， `tapPromise` | `callAsync`， `promise` | 第一个非`undefined` 价值,或者 `undefined` | 使钩子短路 |
| `AsyncSeriesLoopHook` | `tap`， `tapAsync`， `tapPromise` | `callAsync`， `promise` | `undefined` | 非`undefined` 从第一次点击开始重新启动循环 |
| `AsyncSeriesWaterfallHook` | `tap`， `tapAsync`， `tapPromise` | `callAsync`， `promise` | 最终值（最后一次点击后的第一个参数） | 作为第一个参数传递给下一个点击 |

每个类的详细行为：

### SyncHook

一个基本的同步钩子。每个被选中的函数都是按注册顺序调用的，并带有传递给它的参数 `call`． 调用函数的返回值将被忽略 `call` 返回 `undefined`。

-   利用方法: `tap`
-   调用方法: `call`
-   `tapAsync` 和 `tapPromise` 抛出错误。

```js
const hook = new SyncHook(["name"]);
hook.tap("A", (name) => console.log(`hello ${name}`));
hook.tap("B", (name) => console.log(`hi ${name}`));
hook.call("world");
// hello world
// hi world
```

### SyncBailHook

允许提前退出的同步钩子。每个选中的函数都按顺序调用，直到返回一个非`undefined` 价值;这个值变成的结果 `call` 剩下的点击会被跳过。如果所有轻击都返回 `undefined`， `call` 返回 `undefined`。

-   利用方法: `tap`
-   调用方法: `call`

```js
const hook = new SyncBailHook(["value"]);
hook.tap("Negative", (v) => (v < 0 ? "negative" : undefined));
hook.tap("Zero", (v) => (v === 0 ? "zero" : undefined));
hook.tap("Positive", (v) => "positive");

hook.call(-1); // "negative" (later taps skipped)
hook.call(5); // "positive"
```

### SyncWaterfallHook

一种同步钩子，通过其抽头函数来执行一个值。传递给的第一个参数 `call` 被转发到第一次点击。如果轻击返回非-`undefined` 值，它将为下一次点击替换该参数；否则，保留之前的值。 `call` 返回最后一个抽头运行后的值。额外的参数（如果有的话）将不加更改地传递。

-   利用方法: `tap`
-   调用方法: `call`

```js
const hook = new SyncWaterfallHook(["value"]);
hook.tap("Double", (v) => v * 2);
hook.tap("PlusOne", (v) => v + 1);

hook.call(3); // 7  -> (3 * 2) + 1
```

### SyncLoopHook

一种同步钩子，它不停地重新运行它的水龙头，直到所有水龙头都返回为止 `undefined` 要一张完整的通行证。每当轻击返回非-时`undefined` 钩子从第一次点击开始重新启动。 `call` 返回 `undefined`。

-   利用方法: `tap`
-   调用方法: `call`

```js
const hook = new SyncLoopHook(["state"]);
let retries = 3;
hook.tap("Retry", () => {
    if (retries-- > 0) return true; // non-undefined restarts the loop
});
hook.tap("Log", () => console.log("pass"));

hook.call({});
// pass (runs once all taps return undefined)
```

### AsyncParallelHook

一个异步钩子，并行运行它所有的调用函数。当每个tap都发出完成信号（同步返回、回调或承诺解决）时，它完成。返回值和解析值被忽略； `callAsync`的回调被调用时没有结果，并且 `promise()` 解析为 `undefined`． 如果有任何抽头错误，错误将被转发，剩余的抽头仍然完成，但其结果将被丢弃。

-   利用方法: `tap`， `tapAsync`， `tapPromise`
-   调用方法: `callAsync`， `promise`

```js
const hook = new AsyncParallelHook(["source"]);
hook.tapPromise("Fetch", (src) => fetch(src));
hook.tapAsync("Log", (src, cb) => {
    console.log("fetching", src);
    cb();
});

await hook.promise("https://example.com");
```

### AsyncParallelBailHook

就像 `AsyncParallelHook`，但其目的是为了摆脱困境。所有抽头函数并行启动；第一个水龙头产生一个非`undefined` 值（同步的，通过它的回调，或者通过解析它的promise）决定钩子的结果。剩下的水龙头继续运行，但它们的结果被忽略。顺序由轻击注册顺序决定：较早轻击的值优先于较晚轻击的值，即使后一个轻击先完成。

-   利用方法: `tap`， `tapAsync`， `tapPromise`
-   调用方法: `callAsync`， `promise`

```js
const hook = new AsyncParallelBailHook(["key"]);
hook.tapPromise("Cache", async (key) => cache.get(key));
hook.tapPromise("Db", async (key) => db.lookup(key));

const value = await hook.promise("user:42");
// First non-undefined result (by registration order) wins.
```

### AsyncSeriesHook

一种异步钩子，它一个接一个地运行选定的函数，等待每个函数完成后再开始下一个函数。结果被忽略； `callAsync`的回调被调用时没有结果，并且 `promise()` 解析为 `undefined`． 第一个错误将终止该系列。

-   利用方法: `tap`， `tapAsync`， `tapPromise`
-   调用方法: `callAsync`， `promise`

```js
const hook = new AsyncSeriesHook(["request"]);
hook.tapPromise("Authenticate", async (req) => authenticate(req));
hook.tapPromise("Log", async (req) => logger.info(req.url));

await hook.promise(request);
```

### AsyncSeriesBailHook

允许提前退出的异步系列钩子。选中的函数一个接一个地运行；一旦产生非-`undefined` 值，则该值成为钩子的结果，并跳过剩余的点击。

-   利用方法: `tap`， `tapAsync`， `tapPromise`
-   调用方法: `callAsync`， `promise`

```js
const hook = new AsyncSeriesBailHook(["id"]);
hook.tapPromise("Memory", async (id) => memory.get(id));
hook.tapPromise("Disk", async (id) => disk.read(id));

const value = await hook.promise("doc-1");
// Stops at the first tap that produces a value.
```

### AsyncSeriesLoopHook

循环的异步系列钩子。选中的函数一个接一个地运行；每当轻击产生非-时`undefined` 钩子从第一次点击开始重新启动。当一个完整的传递产生时，钩子完成 `undefined` 从每一个水龙头。结果总是 `undefined`。

-   利用方法: `tap`， `tapAsync`， `tapPromise`
-   调用方法: `callAsync`， `promise`

```js
const hook = new AsyncSeriesLoopHook(["job"]);
hook.tapPromise("Process", async (job) => {
    const more = await job.step();
    if (more) return true; // restart the loop
});

await hook.promise(job);
```

### AsyncSeriesWaterfallHook

一个异步系列钩子，通过它的抽头执行一个值。传递给的第一个参数 `callAsync` / `promise` 被转发到第一次点击。水龙头不是`undefined` 返回/回调/解析值为下一次点击替换它； `undefined` 保持先前的值。钩子在最后一次点击后完成该值。

-   利用方法: `tap`， `tapAsync`， `tapPromise`
-   调用方法: `callAsync`， `promise`

```js
const hook = new AsyncSeriesWaterfallHook(["source"]);
hook.tapPromise("Read", async (src) => fs.readFile(src, "utf8"));
hook.tapPromise("Trim", async (text) => text.trim());

const output = await hook.promise("./input.txt");
```

## 拦截

所有钩子都公开一个 `intercept(interceptor)` 方法。拦截器是一个普通对象，它的方法在钩子生命周期中的特定点被调用。拦截器在监听之前按注册顺序调用，对于记录、跟踪、分析或重新映射监听选项很有用。

```js
myCar.hooks.calculateRoutes.intercept({
    name: "LoggingInterceptor",
    call: (source, target, routesList) => {
        console.log("Starting to calculate routes");
    },
    tap: (tapInfo) => {
        // tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ..., stage: 0 }
        console.log(`${tapInfo.name} is running`);
    },
    register: (tapInfo) => {
        // Called once per tap (and for each tap already registered when the
        // interceptor is added). Return a new tapInfo object to replace it.
        console.log(`${tapInfo.name} is registered`);

        return tapInfo;
    }
});
```

| 处理程序 | 签名 | 当它运行时 |
| --- | --- | --- |
| `call` | `(...args) => void` | 在钩子开始执行它的水龙头之前。接收传递给的参数 `call` / `callAsync` / `promise`。 |
| `tap` | `(tap: Tap) => void` | 在每个水龙头运行之前。这个 `tap` 对象是快照-忽略突变。 |
| `loop` | `(...args) => void` | 在a的每次迭代开始时 `SyncLoopHook` / `AsyncSeriesLoopHook`。 |
| `error` | `(err: Error) => void` | 每当tap抛出、拒绝或调用回调时出现错误。 |
| `result` | `(result: any) => void` | 当保释或瀑布钩子产生一个值时，或者当tap为循环钩子产生一个值时。 |
| `done` | `() => void` | 当钩子成功完成时（没有错误，没有提前保释）。 |
| `register` | `(tap: Tap) => Tap | undefined` | 在注册时每次监听一次（包括在添加拦截器之前存在的监听）。返回new `Tap` 对象来替换它。 |
| `name` | `string` | 生态系统调试时使用的可选标签。 |
| `context` | `boolean` | 选择加入共享的 `context` 对象。看到 [上下文](https://npmcn.com/package/tapable/#readme-context)。 |

添加拦截器会使钩子的编译调用函数失效 `call` / `callAsync` / `promise` 重新编译它，以便将新的拦截器编入其中。

## 上下文

插件和拦截器可以选择访问一个可选的 `context` 对象，该对象可用于将任意值传递给后续的插件和拦截器。

```js
myCar.hooks.accelerate.intercept({
    context: true,
    tap: (context, tapInfo) => {
        // tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
        console.log(`${tapInfo.name} is doing it's job`);

        // `context` starts as an empty object if at least one plugin uses `context: true`.
        // If no plugins use `context: true`, then `context` is undefined.
        if (context) {
            // Arbitrary properties can be added to `context`, which plugins can then access.
            context.hasMuffler = true;
        }
    }
});

myCar.hooks.accelerate.tap(
    {
        name: "NoisePlugin",
        context: true
    },
    (context, newSpeed) => {
        if (context && context.hasMuffler) {
            console.log("Silence...");
        } else {
            console.log("Vroom!");
        }
    }
);
```

## HookMap

一个 `HookMap` 是一个帮助器类，它惰性地为每个键创建钩子。构造函数接受一个工厂函数；第一次通过请求密钥 `for(key)`，则调用工厂，并缓存生成的钩子。

```js
const keyedHook = new HookMap((key) => new SyncHook(["arg"]));
```

插件使用 `for(key)` 获取特定键的钩子（按需创建），然后 `tap` 像往常一样：

```js
keyedHook.for("some-key").tap("MyPlugin", (arg) => {
    /* ... */
});
keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => {
    /* ... */
});
keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => {
    /* ... */
});
```

的所有者 `HookMap` 使用 `get(key)` 查找现有的钩子而不创建钩子。这通常是在调用端首选的，这样没有插件关心的键永远不会被实现：

```js
const hook = keyedHook.get("some-key");
if (hook !== undefined) {
    hook.callAsync("arg", (err) => {
        /* ... */
    });
}
```

一个 `HookMap` 也可以被拦截。 `intercept({ factory })` 包装工厂，以便您可以自定义或替换为每个新键返回的钩子。

## 代码生成

Tapable不会在调用时迭代轻击。相反，是第一次 `call`， `callAsync` 或 `promise` 在钩子被修改后调用，钩子会使用 `new Function(...)` 并在实例上缓存它。这就是README中“代码中的eval”的含义：钩子的调度逻辑作为字符串生成，并转化为引擎可以内联和优化的真正的JavaScript函数。

生成的函数被定制为：

-   **调用类型** -主人是否打来电话 `call` (同步) `callAsync` (回调),或 `promise`． 每一种都会产生不同的骨架。 `promise()` 把尸体裹起来 `new Promise((_resolve, _reject) => { ... })`。
-   **利用类型** -对于每次点击，生成器都会发出正确的调用模式：直接调用 `tap`的节点式回调包装 `tapAsync`, `.then(...)` 链接为 `tapPromise`。
-   **钩类** - - - - - - `SyncHook` 发出直线序列的呼叫； `SyncBailHook` 发出提前返回支票； `SyncWaterfallHook` 通过调用线程化一个值；循环钩子将主体包裹在一个重新进入的循环中； `AsyncParallel*` 扇出水龙头并计算完井次数； `AsyncSeries*` 链。
-   **拦截器** -如果附加了拦截器，调用它们的 `call`/`tap`/`loop`/`error`/`result`/`done` 处理程序被拼接到生成的主体中；否则，它们不需要任何成本。
-   **上下文** ——一个 `_context` 对象只有在至少有一个tap或拦截器选择使用 `context: true`。
-   **参数数量** -生成的代码硬编码了当钩子被构造时声明的参数的数量，所以没有 `arguments`/rest处理发生在运行时。

每当钩子的形状发生变化时，编译的函数就失效（重置回一次性“重新编译然后调用”trampoline） `tap*` 或 `intercept` 呼叫。因此，稳态调用直接通过缓存的函数运行，没有每个tap分支。

### 为什么这很重要？

-   您只需为您使用的功能付费。一个没有拦截器、只同步的钩子编译成一个简短的直接函数调用序列。
-   调试钩子意味着读取生成的源代码。如果你想看的话， `Hook.prototype.compile` 返回 `new Function(...)` 结果-日志 `hook._createCall("sync").toString()` (或 `"async"` / `"promise"`)检查尸体。
-   因为分派是由代码生成的，所以钩子的行为是在编译时完全确定的。在注册后改变点击选项(例如，更改 `stage` 在现有的 `Tap` 对象)在引起重新编译之前不会对抽头重新排序。

## 钩/ HookMap接口

Public（任何持有钩子引用的人都可以调用，比如插件）：

```ts
interface Hook {
    tap: (name: string | Tap, fn: (context?, ...args) => Result) => void;
    tapAsync: (
        name: string | Tap,
        fn: (
            context?,
            ...args,
            callback: (err: Error | null, result: Result) => void
        ) => void
    ) => void;
    tapPromise: (
        name: string | Tap,
        fn: (context?, ...args) => Promise<Result>
    ) => void;
    intercept: (interceptor: HookInterceptor) => void;
    withOptions: (
        options: TapOptions
    ) => Omit<Hook, "call" | "callAsync" | "promise">;
}

interface HookInterceptor {
    name?: string;
    call?: (context?, ...args) => void;
    loop?: (context?, ...args) => void;
    tap?: (context?, tap: Tap) => void;
    error?: (err: Error) => void;
    result?: (result: any) => void;
    done?: () => void;
    register?: (tap: Tap) => Tap | undefined;
    context?: boolean;
}

interface HookMap {
    for: (key: any) => Hook;
    intercept: (interceptor: HookMapInterceptor) => void;
}

interface HookMapInterceptor {
    factory: (key: any, hook: Hook) => Hook;
}

interface Tap {
    name: string;
    type: "sync" | "async" | "promise";
    fn: Function;
    stage: number;
    context: boolean;
    before?: string | Array<string>;
}
```

受保护（只针对包含钩子的类——它拥有触发钩子的权利）：

```ts
interface Hook {
    isUsed: () => boolean;
    call: (...args) => Result;
    promise: (...args) => Promise<Result>;
    callAsync: (
        ...args,
        callback: (err: Error | null, result: Result) => void
    ) => void;
}

interface HookMap {
    get: (key: any) => Hook | undefined;
    for: (key: any) => Hook;
}
```

`isUsed()` 返回 `true` 当钩子至少注册了一个tap或interceptor时。当没有插件在监听时，钩子所有者可以使用它来跳过昂贵的参数准备：

```js
class Car {
    // ...
    setSpeed(newSpeed) {
        if (this.hooks.accelerate.isUsed()) {
            this.hooks.accelerate.call(newSpeed);
        }
    }
    // ...
}
```

## MultiHook

一个 `MultiHook` 是一个向前的钩子式立面 `tap`， `tapAsync`， `tapPromise`， `intercept`, `withOptions` 同时连接几个底层钩子。它不会暴露 `call*` 方法——只有包装钩子的所有者决定它们何时运行。这是类公开“发生在这些事件中的任何一个”监听表面的典型方式，而无需插件将自己单独连接到每个钩子。

### 把一个龙头向几个钩子扇形展开

```js
const { MultiHook, SyncHook } = require("tapable");

class Car {
    constructor() {
        const accelerate = new SyncHook(["newSpeed"]);
        const brake = new SyncHook();
        this.hooks = {
            accelerate,
            brake,
            // `anyMovement` is not a real hook — it simply re-registers taps
            // on both `accelerate` and `brake`.
            anyMovement: new MultiHook([accelerate, brake])
        };
    }
}

const car = new Car();
car.hooks.anyMovement.tap("Telemetry", () => console.log("car moved"));

car.hooks.accelerate.call(42); // "car moved"
car.hooks.brake.call(); // "car moved"
```

这个 `MultiHook` 没有自己的状态：上面的水龙头在里面结束 `accelerate.taps` 和 `brake.taps`。

### 转发异步轻接

`tapAsync` / `tapPromise` 转发到每个包装的钩子——确保它们都是兼容的是插件的工作。注册一个 `tapPromise` 在一个 `MultiHook` 这就结束了。 `SyncHook` 将在注册时间抛出钩子。

```js
const build = new AsyncSeriesHook(["stats"]);
const rebuild = new AsyncSeriesHook(["stats"]);
const anyBuild = new MultiHook([build, rebuild]);

anyBuild.tapPromise("Report", async (stats) => report.send(stats));
```

### 共享拦截器和选项

`intercept` 和 `withOptions` 也都转发了，所以呢 `MultiHook` 可以用来将相同的拦截器或预配置的选项附加到一组钩子上：

```js
const anyBuild = new MultiHook([build, rebuild]);

anyBuild.intercept({
    call: () => console.log("build started"),
    done: () => console.log("build done")
});

// Every tap added through `late` is staged late on both underlying hooks.
const late = anyBuild.withOptions({ stage: 10 });
late.tap("RunLast", () => {
    /* ... */
});
```

### `isUsed`

`isUsed()` 返回 `true` 如果任何一个包裹的钩子至少有一个tap或interceptor，当没有人在监听它们时，所有者可以便宜地跳过工作：

```js
if (this.hooks.anyMovement.isUsed()) {
    // expensive telemetry payload is only built when a plugin actually cares
    this.hooks.accelerate.call(computeSpeed());
}
```

## 常见问题

### tapable 是什么？

只是一个插件的小模块。

### 如何安装 tapable？

在项目目录运行 npm install tapable，或使用 pnpm add / yarn add / bun add。

### tapable 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### tapable 支持 ESM 还是 CommonJS？

以 CommonJS 形式发布，可以直接 require\(\)；在 ES Module 项目中也可以用 import 引入（Node\.js 会把 module\.exports 作为默认导出）。

### tapable 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=6。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### tapable 可以商用吗？

tapable 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### tapable 的下载量有多少？

根据 npm 官方下载统计，tapable 在 2026/10/07 – 2026/11/05 这 30 天内被下载 626,296,851 次，日均约 20,876,562 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### tapable 还在维护吗？

最新版本 2\.3\.3 发布于 2026/04/21（6 个月前）；至今共发布 54 个版本，其中过去 12 个月发布了 3 个；首次发布于 2013/01/21。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

