likes
comments
collection
share

推开Webpack事件流大门——Tapable

作者站长头像
站长
· 阅读数 19

前言

Tapable是一个由Webpack团队维护的事件库。它基于发布订阅模式实现事件处理,可以在Webpack的打包流程中插入自定义逻辑,解耦不同插件之间的处理流程。Webpack中Compiler和Compilation是Webpack内置对象,它们都继承于Tapable,通过使用Tapable来触发事件,从而将不同插件串联起来。我们可以使用Webpack提供的事件钩子来在插件中添加对应的事件处理逻辑。废话少说,入正题!

Hook Types

下面介绍下Hooks的几种类型的概念。

  • Waterfall(瀑布类型)将上一个函数的返回值传递给下一个函数的第一个参数,如果没有返回值,则使用call函数的参数

  • Bail(保险类型)如果有返回值时,则退出任务队列

  • Loop(循环类型)如果有函数的返回值不是undefined,则从头开始执行

  • Sync(同步)函数顺序执行

  • AsyncSeries(异步串行)异步函数按顺序执行

  • AsyncParallel(异步并行)异步函数并行

推开Webpack事件流大门——Tapable

Hooks

SyncHook

同步顺序执行

import { SyncHook } from "./lib";

const syncHook = new SyncHook(["name", "age"]);

syncHook.tap("test1", (name, age) => {
    console.log("test1", name, age);
});

syncHook.tap("test2", (name, age) => {
    console.log("test2", name, age);
});

syncHook.tap("test3", (name, age) => {
    console.log("test3", name, age);
});

syncHook.call("szy", "18");
test1 szy 18
test2 szy 18
test3 szy 18

SyncBailHook

同步顺序执行,返回结果后不往下执行

import {SyncBailHook} from './lib';

const syncBailHook = new SyncBailHook(["name", "age"]);

syncBailHook.tap("test1", (name, age) => {
  console.log("test1", name, age);
});

syncBailHook.tap("test2", (name, age) => {
    console.log("test2", name, age);
    return 'result'
});

syncBailHook.tap("test3", (name, age) => {
  console.log("test3", name, age);
});

syncBailHook.call("szy", "18");
test1 szy 18
test2 szy 18

SyncWaterfallHook

同步顺序执行,返回结果是下一个函数的第一个参数,无返回则是call的参数

import { SyncWaterfallHook } from "./lib";

const syncWaterfallHook = new SyncWaterfallHook(["name", "age"]);

syncWaterfallHook.tap("test1", (name, age) => {
    console.log("test1", name, age);
});

syncWaterfallHook.tap("test2", (name, age) => {
    console.log("test2", name, age);
    return "test2";
});

syncWaterfallHook.tap("test3", (name, age) => {
    console.log("test3", name, age);
});

syncWaterfallHook.call("szy", "18");
test1 szy 18
test2 szy 18
test3 test2 18

SyncLoopHook

同步顺序执行,知道所有的函数全部返回undefined,否则重头执行

import { SyncLoopHook } from "./lib";

const syncLoopHook = new SyncLoopHook();

let count = 3;

syncLoopHook.tap("test1", () => {
    console.log("test1...   ", count);
    if (count < 3) {
        return;
    }
    return count--;
});

syncLoopHook.tap("test2", () => {
    console.log("test2...   ", count);
    if (count < 2) {
        return;
    }
    return count--;
});

syncLoopHook.tap("test3", () => {
    console.log("test3...   ", count);
    if (count < 1) {
        return;
    }
    return count--;
});

syncLoopHook.call();
test1...    3
test1...    2
test2...    2
test1...    1
test2...    1
test3...    1
test1...    0
test2...    0
test3...    0

AsyncParallelHook(回调形式)

特点:异步,并行

  • 使用tapAsync注册,callAsync触发
  • 注册时需要传入cb回调,代表函数完成
  • callAsync的回调
    • 如果所有的cb全是undefined,将在注册函数完成后执行
    • 如果其中有一个cb传入error,所有的注册函数因并行还是会全部执行,但是callAsync的回调会在第一个cb(err)时结束
import { AsyncParallelHook } from "./lib";

const asyncParallelHook = new AsyncParallelHook(["name", "age"]);

asyncParallelHook.tapAsync("test1", (name, age, cb) => {
    console.log("test1入口");
    setTimeout(() => {
        console.log("test1", name, age);
        cb();
        // cb({ success: false, name: "test1" });
    }, 2000);
});

asyncParallelHook.tapAsync("test2", (name, age, cb) => {
    console.log("test2入口");
    setTimeout(() => {
        console.log("test2", name, age);
        cb();
        // cb({ success: false, name: "test2" });
    }, 3000);
});

asyncParallelHook.tapAsync("test3", (name, age, cb) => {
    console.log("test3入口");
    setTimeout(() => {
        console.log("test3", name, age);
        cb();
    }, 1000);
});

asyncParallelHook.callAsync("szy", "18", data => {
    console.log("end", data);
});
test1入口
test2入口
test3入口
test3 szy 18
test1 szy 18
test2 szy 18
end undefined
test1入口
test2入口
test3入口
test3 szy 18
test1 szy 18
end { success: false, name: 'test1' }
test2 szy 18

AsyncSeriesHook(回调形式)

特点:异步,串行

  • 使用tapAsync注册,callAsync触发
  • 注册时需要传入cb回调,代表函数完成
  • callAsync的回调
    • 如果所有的cb全是undefined,将在注册函数完成后执行
    • 如果其中有一个cb传入error,则后续注册函数不再执行
import { AsyncSeriesHook } from "./lib";

const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);

asyncSeriesHook.tapAsync("test1", (name, age, cb) => {
    console.log("test1入口");
    setTimeout(() => {
        console.log("test1", name, age);
        cb();
    }, 2000);
});

asyncSeriesHook.tapAsync("test2", (name, age, cb) => {
    console.log("test2入口");
    setTimeout(() => {
        console.log("test2", name, age);
        cb();
    // cb({ success: false });
    }, 3000);
});

asyncSeriesHook.tapAsync("test3", (name, age, cb) => {
    console.log("test3入口");
    setTimeout(() => {
        console.log("test3", name, age);
        cb();
    }, 1000);
});

asyncSeriesHook.callAsync("szy", "18", err => {
    console.log("end", err);
});
test1入口
test1 szy 18
test2入口
test2 szy 18
test3入口
test3 szy 18
end undefined
test1入口
test1 szy 18
test2入口
test2 szy 18
end { success: false }

AsyncSeriesBailHook(Promise)

特点:异步,串行,保险类型

  • 使用tapPromise注册,promise触发
  • 如果遇到resolve有返回值时,退出当前任务
import { AsyncSeriesBailHook } from "./lib";

const asyncSeriesBailHook = new AsyncSeriesBailHook(["name", "age"]);

asyncSeriesBailHook.tapPromise("test1", (name, age) => {
    console.log("test1入口");
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("test1", name, age);
            resolve();
        }, 2000);
    });
});

asyncSeriesBailHook.tapPromise("test2", (name, age) => {
    console.log("test2入口");
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("test2", name, age);
            resolve({ success: true });
        }, 3000);
    });
});

asyncSeriesBailHook.tapPromise("test3", (name, age, cb) => {
    console.log("test3入口");
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("test3", name, age);
            resolve();
        }, 1000);
    });
});

asyncSeriesBailHook.promise("szy", "18").then(res => {
    console.log(res);
});
test1入口
test1 szy 18
test2入口
test2 szy 18
{ success: true }

其他Hook

以上总结了同步、异步,已经瀑布、保险、循环类型的hook调用示例。

AsyncParallelBailHookAsyncSeriesWaterfallHookAsyncSeriesLoopHook

剩下的这几个函数也可以参照上述栗子🌰,自己实现下~

Before/Stage

在tap注册函数时,第一个参数可以是一个对象,name是注册名,before和stage决定了所有注册函数的顺序

Before

import { SyncHook } from "./lib";

const syncHook = new SyncHook(["name", "age"]);

syncHook.tap(
  {
    name: "test1"
  },
  (name, age) => {
    console.log("test1", name, age);
  }
);

// 将test2提前于test1
syncHook.tap(
  {
    name: "test2",
    before: "test1"
  },
  (name, age) => {
    console.log("test2", name, age);
  }
);

syncHook.tap(
  {
    name: "test3"
  },
  (name, age) => {
    console.log("test3", name, age);
  }
);

syncHook.call("szy", "18");
test2 szy 18
test1 szy 18
test3 szy 18

Stage

import { SyncHook } from "./lib";

const syncHook = new SyncHook(["name", "age"]);

syncHook.tap(
    {
        name: "test1",
        stage: 0
    },
    (name, age) => {
        console.log("test1", name, age);
    }
);

syncHook.tap(
    {
        name: "test2",
        stage: 1
    },
    (name, age) => {
        console.log("test2", name, age);
    }
);

syncHook.tap(
    {
        name: "test3",
        stage: -1
    },
    (name, age) => {
        console.log("test3", name, age);
    }
);

syncHook.call("szy", "18");
test3 szy 18
test1 szy 18
test2 szy 18

stage默认值是0,支持负数,从小到大执行

⚠️ 如果同时设置before和stage,会优先执行before逻辑,也就是说before优先级更高。

Hook拦截器

下面我们看一个loop典型函数的拦截过程

import { SyncLoopHook } from "./lib";

const syncLoopHook = new SyncLoopHook();

syncLoopHook.intercept({
    // register: tap函数注册完触发
    register: tap => {
        console.log(tap.name + "register拦截器");
        return tap;
    },
    // call: call函数执行后触发
    call: (...args) => {
        console.log("call拦截器");
    },
    // tap: tap中回调函数前触发
    tap: tap => {
        console.log("tap拦截器");
    },
    // loop: loop类型函数在回调前触发
    loop: (...args) => {
        console.log("loop拦截器");
    }
});

// 省略syncLoopHook.tap的注册过程
syncLoopHook.call();

推开Webpack事件流大门——Tapable

HookMap

HookMap本质上是一个辅助类,提供了for、get等实例方法,我们可以通过HookMap更好的分类管理

还是SyncHook这个栗子:

import { HookMap, SyncHook } from "./lib";

const hookMap = new HookMap(key => {
    console.log(key);
    return new SyncHook(["name", "age"]);
});

hookMap.for("key1").tap("test1", (name, age) => {
    console.log("key1: test1", name, age);
});

hookMap.for("key2").tap("test2-1", (name, age) => {
    console.log("key2: test2-1", name, age);
});

hookMap.for("key2").tap("test2-2", (name, age) => {
    console.log("key2: test2-2", name, age);
});

hookMap.for("key3").tap("test3", (name, age) => {
    console.log("key3: test3", name, age);
});

const hook1 = hookMap.get("key2");
hook1.call("szy", 18);
key1
key2
key3
key2: test2-1 szy 18
key2: test2-2 szy 18
  1. 可以看到HookMap接受一个函数,参数是for方法存入的key值,返回一个tapable hook。
  2. 通过HookMap的实例方法for去创建一个分组(key2),等到一个Hook类的实例。
  3. 给实例添加tap、tapAsync等方法。
  4. 通过HookMap的实例方法get获取当前分组(key2)下的Hook对象。
  5. 调用Hook对象...

MultiHook

MultiHook主要就是将多个hook整合在一起,可以批量拦截和注册(tap/tapAsync/tapPromise)。

this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);

实现原理

下面以SyncHook函数讲解下tapable的主要执行过程

创建Hook实例

我们看下new SyncHook的构建过程。

const syncHook = new SyncHook(["name", "age"]);
function SyncHook(args = [], name = undefined) {
    const hook = new Hook(args, name);
    hook.constructor = SyncHook;
    hook.tapAsync = TAP_ASYNC; // 异步函数抛出错误
    hook.tapPromise = TAP_PROMISE; // Promise函数抛出错误
    hook.compile = COMPILE;
    return hook;
}

Hook类主要创建了taps数组(所有tap事件)、参数name、age的收集、所有的拦截器,还有就是注册和触发的事件(tap、call...),至于这个_x我们之后会说到。

class Hook {
  constructor(args = [], name = undefined) {
    this._args = args;
    this.name = name;
    this.taps = [];
    this.interceptors = [];
    this._call = CALL_DELEGATE;
    this.call = CALL_DELEGATE;
    this._callAsync = CALL_ASYNC_DELEGATE;
    this.callAsync = CALL_ASYNC_DELEGATE;
    this._promise = PROMISE_DELEGATE;
    this.promise = PROMISE_DELEGATE;
    this._x = undefined;

    this.compile = this.compile;
    this.tap = this.tap;
    this.tapAsync = this.tapAsync;
    this.tapPromise = this.tapPromise;
  }
  ...
}

接下来就是compile这个抽象类,他会由继承Hook的子类实现,比如SyncHook就是这么实现的:

const COMPILE = function(options) {
    factory.setup(this, options);
    return factory.create(options);
};

这边的实现过程可以略过,后面会讲到😜

tap注册事件

接下来我们一起探索下tap事件的注册过程

syncHook.tap("test1", (name, age) => {
    console.log("test1", name, age);
});

首先进入Hook类中的_tap函数

将同步函数类型,test1,处理的回调函数传入

// 注明同步函数类型
this._tap("sync", options, fn);
_tap(type, options, fn) {
    ...
        // 一些options的整合处理
        options = Object.assign({ type, fn }, options);
    // 执行register拦截器
        options = this._runRegisterInterceptors(options);
        this._insert(options);
    }
  • _tap函数内部将options整合,以及执行了所有拦截器中的register。
  • _insert函数会根据tap注册事件时我们传入的before和stage,对所有的事件进行排序,插入Hook类的taps数组。

call函数调用

到了最后的调用函数

syncHook.call("szy", "18");
  1. 会进入到_createCall,声明同步类型
this.call = this._createCall("sync");
  1. 进入上文的SyncHook自己的COMPILE
setup(instance, options) {
    instance._x = options.taps.map(t => t.fn);
}
// 动态创建call函数
factory.create(options);
  • 将所有的taps数组中存的事件放入实例的_x属性中(此时taps的执行已经排过序)
  • 执行create方法

create

这边就用到了tapable中的另一个核心类,HookCodeFactory工厂函数。我们来看看create的实现

fn = new Function(
  this.args(),
  '"use strict";\n' +
    this.header() +
    this.contentWithInterceptors({
      onError: err => `throw ${err};\n`,
      onResult: result => `return ${result};\n`,
      resultReturns: true,
      onDone: () => "",
      rethrowIfPossible: true
    })
);

这边使用了new Function来动态创建了call的执行函数,(用Function的主要原因是因为性能方面的考虑,在大量使用的场景下性能会比较好,具体可以参考github.com/webpack/tap…)。

具体步骤如下:

  • 获取调用参数(args数组),赋值给Function的参数列表
  • header函数对内部的_x函数列表和拦截器赋值
  • contentWithInterceptors函数
    • 执行call拦截器
    • 遍历taps数组,以下标获取_x列表中对应的函数
    • 将args参数列表的形参带入_x函数的实参(对应了第一步的Function参数)

最终生成了什么❓

(function anonymous(name, age
) {
  "use strict";
  var _context;
  var _x = this._x;
  var _fn0 = _x[0];
  _fn0(name, age);
  var _fn1 = _x[1];
  _fn1(name, age);
  var _fn2 = _x[2];
  _fn2(name, age);
})

总结

  1. tapable中除了同步执行函数,也提供Waterfall、Bail、Loop类型的函数,在支持同步的基础上添加了异步的串行与并行函数
  2. tapable也提供了一系列的辅助函数、拦截器等功能。
  3. 通过对SyncHook函数的源码解析,对tapable内部实现得到清晰的了解。

参考链接