Tapable的神秘之处-源码解析(1)

123 阅读8分钟

Tapable的神秘之处-源码解析(1)

前言:研究webpack过程中,发现tapable在其中占据了很重要的成分,所以就看看它的奥妙之处

Tapable是一个在webpack中被广泛使用的核心模块,它提供了一组灵活的钩子函数,可以在不同的生命周期中插入自定义的逻辑。通过使用Tapable,我们可以轻松地实现各种功能,从简单的插件拓展到复杂的编译过程优化。在这篇文章中,我们将深入探索Tapable的神秘之处,了解它的底层实现。

从官方文档可以看到tapable提供以下方法

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

为了弄清楚这些方法都是有什么秘密可言,让我们开始从tapable仓库开始探究吧。

仓库地址:github.com/webpack/tap…

源码目录结构

tapable
├─ .babelrc
├─ .editorconfig
├─ .eslintrc
├─ .gitattributes
├─ .gitignore
├─ .prettierrc.js
├─ .travis.yml
├─ README.md
├─ lib 
│  ├─ AsyncParallelBailHook.js
│  ├─ AsyncParallelHook.js
│  ├─ AsyncSeriesBailHook.js
│  ├─ AsyncSeriesHook.js
│  ├─ AsyncSeriesLoopHook.js
│  ├─ AsyncSeriesWaterfallHook.js
│  ├─ Hook.js
│  ├─ HookCodeFactory.js
│  ├─ HookMap.js
│  ├─ index.js
│  ├─ MultiHook.js
│  ├─ SyncBailHook.js
│  ├─ SyncHook.js
│  ├─ SyncLoopHook.js
│  ├─ SyncWaterfallHook.js
│  ├─ util-browser.js
│  └─ __tests__
│     ├─ AsyncParallelHooks.js
│     ├─ AsyncSeriesHooks.js
│     ├─ Hook.js
│     ├─ HookCodeFactory.js
│     ├─ HookStackOverflow.js
│     ├─ HookTester.js
│     ├─ MultiHook.js
│     ├─ SyncBailHook.js
│     ├─ SyncHook.js
│     ├─ SyncHooks.js
│     ├─ SyncWaterfallHook.js
│     └─ __snapshots__
│        ├─ AsyncParallelHooks.js.snap
│        ├─ AsyncSeriesHooks.js.snap
│        ├─ HookCodeFactory.js.snap
│        └─ SyncHooks.js.snap
├─ LICENSE
├─ package.json
├─ tapable.d.ts
└─ yarn.lock

可以看出tapable的源码方法都是放置在lib文件夹中,lib/index.js就是tapable入口文件,并且除了10个对应的Hook文件,还有其他几个方法文件。

今天我们先来看看 Hook.js这个文件到底做了那些有意义的事

核心实现方法lib/Hook.js

代码里定义个一个Hook类的构造函数 接受两个可选参数:args(参数数组)和name(Hook的名称)。在构造函数中,初始化了一些实例属性,包括_args、name、taps、interceptors等,并设置了默认的调用方法(call、callAsync、promise)

初始化配置部分
constructor(args = [], name = undefined) {
    this._args = args; // 参数配置数组
    this.name = name; // Hook的名称
    this.taps = []; // 用于存储注册到 Hook 实例上的 tap 对象。
    this.interceptors = []; // 用于存储注册到 Hook 实例上的拦截器对象
    // 将全局常量 CALL_DELEGATE 赋值给实例属性 _call、call。表示某个调用的委托函数或逻辑。
    this._call = CALL_DELEGATE;
    this.call = CALL_DELEGATE;
    // 将全局常量 CALL_ASYNC_DELEGATE 赋值给实例属性_callAsync、callAsync。表示某个【异步】调用的委托函数或逻辑。
    this._callAsync = CALL_ASYNC_DELEGATE;
    this.callAsync = CALL_ASYNC_DELEGATE;
    // 将全局常量 PROMISE_DELEGATE 赋值给实例属性_promise、promise。表示某个【Promise】调用的委托函数或逻辑。
    this._promise = PROMISE_DELEGATE;
    this.promise = PROMISE_DELEGATE;
    // 暂时没看到具体用途,可能在后续的方法中确定。
    this._x = undefined;
    // compile抽象方法的实现,强制派生类/子类必须被重写以提供具体的实现。
    this.compile = this.compile;
    
    this.tap = this.tap;
    this.tapAsync = this.tapAsync;
    this.tapPromise = this.tapPromise;
}

这段代码定义了一个注册机制,支持不同类型的注册方式(同步、异步、基于Promise的异步),通过对外提供的 tap, tapAsync, 和 tapPromise 方法来实现。它主要用于事件或插件系统中,允许开发者根据不同需求注册回调函数。

_tap方法

_tap(type, options, fn) {
    if (typeof options === "string") {
        options = {
            name: options.trim()
        };
    } else if (typeof options !== "object" || options === null) {
        throw new Error("Invalid tap options");
    }
    if (typeof options.name !== "string" || options.name === "") {
        throw new Error("Missing name for tap");
    }
    if (typeof options.context !== "undefined") {
        deprecateContext();
    }
    options = Object.assign({ type, fn }, options);
    options = this._runRegisterInterceptors(options);
    this._insert(options);
}
​
tap(options, fn) {
    this._tap("sync", options, fn);
}
​
tapAsync(options, fn) {
    this._tap("async", options, fn);
}
​
tapPromise(options, fn) {
    this._tap("promise", options, fn);
}
  • _tap(type, options, fn) 方法是这个注册机制的内核。
  • 参数 type 指明了注册的类型,可以是 "sync", "async", 或 "promise"
  • 参数 options 可以是一个字符串或一个对象。如果是字符串,将被转换成一个对象,对象中有一个 name 属性。如果是一个对象,它必须至少包含一个名为 name 的字符串属性。options 还可以包含一个可选的 context 属性,若存在,则调用 deprecateContext() 函数(可能用于标记该属性为过时)。
  • 参数 fn 是一个将被注册的函数。
  • 方法内部,首先检查 options 参数的有效性,然后将 typefn 加入到 options 对象中,最后通过 _runRegisterInterceptors 方法处理 options,并通过 _insert 方法将处理后的 options 插入到注册列表中。

tap, tapAsync, tapPromise 方法:

这三个方法是 _tap 方法的封装,分别对应不同的 type 参数:

  • tap(options, fn) 用于注册同步类型的回调。
  • tapAsync(options, fn) 用于注册异步类型的回调。
  • tapPromise(options, fn) 用于注册基于Promise的异步回调。

使用场景:

这种模式广泛用于插件系统和事件处理系统,例如Webpack的插件系统就是一个典型的例子。通过这种方式,开发者能够以灵活的方式扩展或修改应用程序的行为,同时保持代码的整洁和组织。每种类型的回调提供了不同的处理方式,以适应不同的执行环境和需求:

  • 使用 tap 注册的回调将同步执行,适用于执行时间短、不涉及IO操作的场景。
  • 使用 tapAsync 注册的回调将异步执行,但需要回调函数来通知执行完成,适用于涉及IO操作等可能需要等待的场景。
  • 使用 tapPromise 注册的回调期望返回一个Promise,适用于现代异步处理,特别是在使用async/await语法时。

_runRegisterInterceptors方法

_runRegisterInterceptors(options) {
    for (const interceptor of this.interceptors) {
        if (interceptor.register) {
            const newOptions = interceptor.register(options);
            if (newOptions !== undefined) {
                options = newOptions;
            }
        }
    }
    return options;
}

这是一个内部方法,用于在注册钩子之前对options对象进行拦截器注册配置。

  • _runRegisterInterceptors(options) 用于在注册钩子之前对options对象进行拦截器注册。。
  • 参数 options注册的钩子对象。
  • 方法内部,对于this.interceptors数组中的每个拦截器,检查interceptor.register是否存在。如果存在,调用interceptor.register方法,并传入options对象。如果interceptor.register方法返回一个新的选项对象newOptions,更新optionsnewOptions。最后返回更新后的options对象。

_runRegisterInterceptors提供了一种扩展和定制回调函数的机制,通过钩子注册和拦截器,可以灵活地控制和改变回调函数的行为

withOptions方法

withOptions(options) {
    // 用于将给定的选项与原始选项进行合并
    const mergeOptions = opt =>
    Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);
​
    return {
        name: this.name,
        tap: (opt, fn) => this.tap(mergeOptions(opt), fn),
        tapAsync: (opt, fn) => this.tapAsync(mergeOptions(opt), fn),
        tapPromise: (opt, fn) => this.tapPromise(mergeOptions(opt), fn),
        intercept: interceptor => this.intercept(interceptor),
        isUsed: () => this.isUsed(),
        withOptions: opt => this.withOptions(mergeOptions(opt))
    };
}

是tapable用于创建一个新的含有特定选项的对象的公共方法

isUsed方法

isUsed() {
    return this.taps.length > 0 || this.interceptors.length > 0;
}

用于检查钩子是否被使用。

intercept方法

intercept(interceptor) {
    // 重置钩子对象的编译状态
    this._resetCompilation();
    // 对interceptor进行复制,并推到interceptors数组里
    this.interceptors.push(Object.assign({}, interceptor));
    if (interceptor.register) {
        for (let i = 0; i < this.taps.length; i++) {
            this.taps[i] = interceptor.register(this.taps[i]);
        }
    }
}

用于注册一个拦截器,并同时将拦截器对象添加到钩子对象taps 数组中。

_resetCompilation方法

_resetCompilation() {
    this.call = this._call;
    this.callAsync = this._callAsync;
    this.promise = this._promise;
}

用于重置钩子对象的编译状态。

_insert方法

_insert(item) {
    this._resetCompilation();
    let before;
    if (typeof item.before === "string") {
        before = new Set([item.before]);
    } else if (Array.isArray(item.before)) {
        before = new Set(item.before);
    }
    let stage = 0;
    if (typeof item.stage === "number") {
        stage = item.stage;
    }
    let i = this.taps.length;
    while (i > 0) {
        i--;
        const x = this.taps[i];
        this.taps[i + 1] = x;
        const xStage = x.stage || 0;
        if (before) {
            if (before.has(x.name)) {
                before.delete(x.name);
                continue;
            }
            if (before.size > 0) {
                continue;
            }
        }
        if (xStage > stage) {
            continue;
        }
        i++;
        break;
    }
    this.taps[i] = item;
}

解析 item.beforeitem.stage

  • 如果 item.before 是字符串类型,将其作为一个元素放入一个新的集合 before 中。
  • 如果 item.before 是数组类型,将其作为多个元素放入一个新的集合 before 中。
  • 如果 item.stage 是数字类型,将其赋值给 stage

从钩子对象的回调函数列表的末尾开始遍历,直到找到合适的插入位置

该方法的目的是根据给定的条件将新的回调函数插入到钩子对象的回调函数列表中。通过调整回调函数的顺序,可以控制它们的执行顺序优先级

结尾

这个 Hook 类为 tapable 提供了以下功能和特性:

  1. 注册和执行回调函数:Hook 类提供了 taptapAsynctapPromise 方法,用于注册回调函数并在适当的时机执行这些回调函数。
  2. 参数配置:可以在 Hook 类的构造函数中传入参数配置数组,用于在执行回调函数时传递参数。
  3. 拦截器:Hook 类支持注册拦截器对象,并在需要时调用拦截器的 register 方法对已注册的回调函数进行修改或替换。
  4. 调用委托函数:Hook 类定义了 _callcall_callAsynccallAsync_promisepromise 等委托函数,用于委托具体的调用行为。
  5. 钩子编译:Hook 类的 compile 方法可以被派生类重写,用于提供具体的编译逻辑,生成最终的调用函数。
  6. 钩子选项:Hook 类提供了 withOptions 方法,用于将给定的选项与原始选项进行合并,并返回一个新的对象,该对象具有与原始 Hook 实例相同的方法,但使用合并后的选项。
  7. 基本信息:Hook 类包含一些基本的信息,如名称(name)和注册的回调函数(taps)。
  8. 可用性检查:Hook 类提供了 isUsed 方法,用于判断当前 Hook 实例是否被使用,即是否注册了回调函数或拦截器对象。

通过这些功能和特性,Hook 类为 tapable 提供了一种机制,用于在不同的时机执行注册的回调函数,并支持对回调函数进行拦截和修改。这为插件系统、事件系统和钩子机制提供了基础框架和工具。