likes
comments
collection
share

💓Tapable:仙贝,请和我交往!💘

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

🔞 四千年一遇!绝世美女 Tabpable 的致命诱惑!

当我们编写自定义 Plugin 时,我们会在 Compiler 对象暴露的钩子上注册事件,然后在 webpack 编译过程中,在合适的时机去执行这些事件,以此达到实现某些自定义逻辑的目的。而在翻阅源码时,我们能发现 webpack 的两个核心对象 CompilerCompilation,但是两大帅哥像失了智一样,竟然都化为舔狗,疯狂 💄追求🌹 一个叫做 Tapable 的女人:

class Compiler extends Tapable{
    // ...
}

class Compilation extends Tapable {
    // ...
}

那这个 Tapable 到底有什么魅力三句话让两个男人为她花了 18 万?!让我们走进今天的《走近科学》栏目

👩‍❤️‍👨 只因你太美!Tapable 的身世居然是这样!

当我翻阅了无数小网站后,终于发现 Tapable 就是一个 发布订阅者模式 的女人,她提供了她的💞秘密联系方式💞,叫做(钩子),而它也让无数男人为之疯狂!集帅们可以根据这些钩子找她约会,并告诉她集帅们精心安排的美妙的行程(自定义事件),在不同的时间段发起猛攻!捕获她的方心!攻陷她的城池!(触发自定义事件)。而 webpack 这个著名舔狗,竟然进行基因改造!在 webpack 内部就通过 Tapable 提前定义好不同阶段的 hook,然后在特定时间触发回调。webpack 他真的,我哭死!🥺

😭 人设崩塌!Tapable 居然有辣么多🪝!

在《让 Tapable 为你倾心的九种方法😘》 这一史书中记载:致命女人 Tapable 提供了9种不同类型的联系方式(9个钩子),它们可以按照同步异步分类、也可以按照执行机制分类(家人们谁懂啊,虾头海王石锤😅)

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

按同步异步分类

按照同步异步分类的话,可以把钩子分成下图所示:

💓Tapable:仙贝,请和我交往!💘

Sync 开头的是同步钩子Async开头的是异步钩子

按执行机制分类

  • Basic(基础钩子):这种类型的钩子,不管事件有没有返回值,会按照事件注册的顺序执行 💓Tapable:仙贝,请和我交往!💘

  • Waterfall(瀑布流钩子):这种类型的钩子,也会按照事件注册的顺序执行,但它与 Basic Hook 不同的是,如果前一个事件存在一个不为 undefined 的返回值 result,那么 result 会传递给下一个事件的第一个参数

    💓Tapable:仙贝,请和我交往!💘

  • Bail(保险钩子):这种类型的钩子,会在其中一个事件有不为 undefined 的返回值 result 时,中断后续事件的执行(胆子小的集帅可以先用这个钩子试探试探!😷)

    💓Tapable:仙贝,请和我交往!💘

  • Loop(循环钩子):这种类型的钩子,当其中一个事件的返回值不为 undefined 时,将会从第一个事件从头开始执行,若返回值是 undefined,才继续执行后续事件(看来这种方式很容易榨干集帅们的钱包啊!☹)

    💓Tapable:仙贝,请和我交往!💘

🥵 饥渴难耐!千金小姐 Tapable 爱上我!

看到这里,相信集帅们已经摸清了 Tapable 的小伎俩!而在 脸如吴彦祖、身材堪比彭于晏、魅力如同陈冠希的集帅们面前,Tapable 竟史上第一次主动提出约会!这不得设计一场美妙绝伦的约会计划让她心跳怦怦、娇羞兴奋🥵!(隔壁 webpack 留下了嫉妒的泪水)

SyncHook

SyncHook 是同步的、按照事件注册顺序执行(不管事件有没有返回值)

const { SyncHook } = require('tapable');

//1、初始化 SyncHook 对象
// 接受一个字符串数组作为参数,数组的长度决定了 tap 注册事件时,callback 函数传入的参数个数
const hook = new SyncHook(["name", "age", "gender"]);

//2、通过 tap 注册事件,在这里,因为 SyncHook 是同步 hook,所以用 tap 注册事件
//该事件的回调函数接受的参数个数就是 初始化时 定义的数组长度
// event1 实际并没有什么意义,只是一个标识位,不要和 eventMitter 里面自定义事件的事件名混淆,它只是一个标识位!!
hook.tap("event1", (name, age, gender) => {
  console.log('event1被执行', name, age, gender);
})

hook.tap("event2", (name, age, gender) => {
  console.log('event2被执行', name, age, gender);
})

//3、触发注册的事件,同步 hook 通过 call 触发
hook.call('Joylne', 23, 'male')

其实可以概括为下面几步:

  • 用你的魅力引蛇出洞:初始化一个 hook 实例对象,接受字符串数组作为参数,数组的长度决定了注册事件时, callback 函数能接受的参数个数
  • 甜蜜嘴遁:对于同步的钩子,使用 tap(Identifier, callback) 函数注册事件,其中,callback 接受的参数个数,就是第一步中字符串数组的长度
  • 相位猛冲:对于同步的钩子,使用 call(...args)触发事件

上面的输出如下:

💓Tapable:仙贝,请和我交往!💘

看着是不是很简单?相信集帅们已经掌握精髓了,甚至不输于练习时长两年半的博主了,那我们继续来会会其余的钩子!

SyncWaterfallHook

SyncWaterfallHook 是同步的瀑布的钩子,按照事件注册顺序执行,当前一个事件有不为 undefined 的返回值 result 时,会把 result 传给下一个事件的第一个参数,如果 result 是 undefined,那么就不管

//  SyncWaterfallHook 是同步的、瀑布流的钩子
//  
//  它会按照注册事件的顺序执行
//  
//  如果上一个钩子有不为 undefined 的返回值,则会这个返回值作为下一个钩子的第一个参数
//  
//  同样,通过 tap 注册事件,call 触发事件
const { SyncWaterfallHook } = require("tapable");

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

hook.tap("event1", (name, age, gender) => {
  console.log("event1被执行了", name, age, gender);
  return "cyt";
});

hook.tap("event2", (name, age, gender) => {
  console.log("event2被执行了", name, age, gender);
  return ["ly", "cdm", "zht"];
});

hook.tap("event3", (name, age, gender) => {
  console.log("event3被执行了", name, age, gender);
});
hook.call("Jolyne", 23, "gender");

图解如下:

💓Tapable:仙贝,请和我交往!💘

输出如下:

💓Tapable:仙贝,请和我交往!💘

SyncBailHook

SyncBailHook 是 同步的保险的 钩子,同样会按照事件注册的顺序执行,当其中有一个事件的返回值 result 不为 undefined 时,会中断后续事件的执行,直接结束。

同样,它以 tap 注册事件、以 call 触发事件

const { SyncBailHook } = require('tapable')

const hook = new SyncBailHook(['name', 'age', 'gender'])

hook.tap('event1', (name, age, gender) => {
  console.log('event1被执行了', name, age, gender);
})

hook.tap('event2', (name, age, gender) => {
  console.log('event2被执行了', name, age, gender);
  // 因为是同步的,所以按照 event1、event2、event3 的顺序执行,当执行到 event2 发现有不为 undefined 的返回值
  // 又因为是 保险的,所以结束后续事件的调用
  return 'event2的返回值'
})

hook.tap('event3', (name, age, gender) => {
  console.log('evnet3被执行了', name, age, gender);
})

hook.call('Joylne', 23, 'male')

输出如下:

💓Tapable:仙贝,请和我交往!💘

图解如下:

💓Tapable:仙贝,请和我交往!💘

SyncLoopHook

SyncLoopHook 是 同步的循环的钩子,同样它会按照事件的注册顺序执行,它会去监听每一个事件的返回值 result 是不是 undefined,如果 result 不是 undefined,则跳回到第一个事件从头执行,如果 result 是 undefined,则执行后续事件

同样,它通过 tap 注册事件,通过 call 触发事件

const { SyncLoopHook } = require('tapable');
let count = 1;
let num = 3;

const hook = new SyncLoopHook(['name', 'age', 'gender'])

hook.tap('event1', (name, age, gender) => {
  console.log('event1 ', count);
  if (count !== 3) {
    return count++
  } else {
    return undefined
  }
})

hook.tap('event2', (name, age, gender) => {
  console.log('event2 ', num);
  if (num !== 1) {
    return num--
  } else {
    return undefined
  }
})
hook.call('Joylne', 23, 'gender')

输出如下:

💓Tapable:仙贝,请和我交往!💘

AsyncSeriesHook

AsyncSeriesHook 是异步的串行的(Series)钩子

对于异步的钩子,通过 tapAsynctapPromise来注册事件:

  • tapAsyc 注册事件时,会多一个 callback 回调,即:

    tapAsync(identifer, (...args, callback) => {
        callback()
    })
    

    此时这个 callback 你可以理解为 antd form 里面的 validator 中的 callback,用来控制事件流程的

    当这个 callback 的第一个参数不为 undefined 时,表示抛出错误,后续的事件不会执行 所以如果要通过 callback 返回值,应该写成: callback(undefined | null, 返回值)

  • tapPromise 注册事件时,通过返回 promise,来控制事件流程

对于异步钩子,通过 callAsyncpromise 一一对应的触发事件

const { AsyncSeriesHook } = require('tapable');

const hook = new AsyncSeriesHook(['name', 'age', 'gender'])

//可以通过 tapAsync 注册事件
hook.tapAsync('event1', (name, age, gender, callback) => {
  console.log('event1 ', name, age, gender);
  // 如果 callback 的第一参数不为 undefined,则相当于抛出错误,event2不会执行
  callback(undefined, '123')
})

//可以通过 tapPromise 注册事件
hook.tapPromise('event2', (name, age, gender) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('event2 ', name, age, gender);
      resolve("cyt")
    }, 2000);
  })
})

// 通过 callAsync 的方式触发事件
hook.callAsync('Joylne', 23, 'male', (err, res) => {
  console.log('结束了', err, res);
})

//输出如下:
event1  Joylne 23 male
event2  Joylne 23 male   //两秒后输出
结束了 undefined undefined

上面我们通过 callAsync 触发事件,我们也可以通过 promise 触发事件

const { AsyncSeriesHook } = require('tapable');

const hook = new AsyncSeriesHook(['name', 'age', 'gender'])

hook.tapAsync('event1', (name, age, gender, callback) => {
  console.log('event1 ', name, age, gender);
  callback(undefined, '123')
})

hook.tapPromise('event2', (name, age, gender) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('event2 ', name, age, gender);
      resolve("cyt")
    }, 2000);
  })
})

//通过 promise 的方式触发事件
hook.promise("Joylne", 23, 'male').then((res) => {
  //对于 AsyncSeriesHook,通过 tapPromise 的形式注册事件后,无法获取 resolve(value) 传递的 value
  console.log('成功了', res);
}).catch((err) => {
  // 通过 promise 的形式触发事件时,callback('嘻嘻') 的嘻嘻会被 catch 捕获
  console.log('失败了', err);
})

//输出如下:
event1  Joylne 23 male
event2  Joylne 23 male  // 两秒后输出
成功了 undefined

如果使用 promise 触发事件,且 event1callback(123),也就是抛出错误,此时会被 catch(err) 的 err 捕获

const { AsyncSeriesHook } = require('tapable');

const hook = new AsyncSeriesHook(['name', 'age', 'gender'])

hook.tapAsync('event1', (name, age, gender, callback) => {
  console.log('event1 ', name, age, gender);
  // callback(undefined, '123')

  //如果传递的第一个参数不是 undefined,相当于抛出了错误,后续的事件不会执行
  callback(123)
})

hook.tapPromise('event2', (name, age, gender) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('event2 ', name, age, gender);
      resolve("cyt")
    }, 2000);
  })
})

//通过 promise 的方式触发事件
hook.promise("Joylne", 23, 'male').then((res) => {
  //对于 AsyncSeriesHook,通过 tapPromise 的形式注册事件后,无法获取 resolve(value) 传递的 value
  console.log('成功了', res);
}).catch((err) => {
  // 通过 promise 的形式触发事件时,callback('嘻嘻') 的嘻嘻会被 catch 捕获
  console.log('失败了', err);
})

//输出如下:
event1  Joylne 23 male
失败了 123

AsyncSeriesBailHook

AsyncSeriesBailHook 是异步的串行的保险的钩子,跟同步保险钩子类似,区别仅在于注册的事件函数是异步函数,同样,如果有不为 undefined 的返回值 result,直接中断后续事件的执行

const { AsyncSeriesBailHook } = require("tapable");
const hook = new AsyncSeriesBailHook(["name", "age", "gender"]);

hook.tapPromise("event1", (name, age, gender) => {
  console.log("event1:", name, age, gender);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve()
    }, 1000);
  });
});

hook.tapAsync("event2", (name, age, gender, callback) => {
  console.log("event2:", name, age, gender);
  setTimeout(() => {
    // callback() 传递的第一个参数如果不是 undefined,则相当于抛出了错误,也会中断后续的事件
    callback(undefined, "cyt");
  }, 1000);
});


hook.tapAsync("event3", (name, age, gender, callback) => {
  console.log("event3:", name, age, gender);
  setTimeout(() => {
    callback();
  }, 1000);
});

// 调用事件并传递执行参数
hook.callAsync("Joylne", 23, "male", (err, res) => {
  console.log("结束了", err, res);
});

// 控制台打印结果:
// event1: Joylne 23 male
// event2: Joylne 23 male
// 结束了 null cyt

AsyncSeriesWaterfallHook

AsyncSeriesWaterfallHook 是 异步的串行的瀑布式的钩子,如果前一个钩子有不为 undefined 的返回值 result,则将 result 作为下一个钩子的第一个入参

const { AsyncSeriesWaterfallHook } = require("tapable");
const hook = new AsyncSeriesWaterfallHook(["name", "age", "gender"]);

hook.tapAsync("event1", (name, age, gender, callback) => {
  console.log("event1:", name, age, gender);
  setTimeout(() => {
    // callback 的第一个参数如果不是 undefined || null 时,相当于会抛出错误
    callback(undefined, 'event1的返回值');
  }, 1000);
});

hook.tapAsync("event2", (name, age, gender, callback) => {
  console.log("event2:", name, age, gender);
  setTimeout(() => {
    callback();
  }, 1000);
});

// 调用事件并传递执行参数;
hook.callAsync("Joylne", 23, "male", (err, res) => {
  console.log("结束了", err, res);
});

//控制台打印结果:
// event1: Joylne 23 male
// event2: event1的返回值 23 male
// 结束了 null event1的返回值

AsyncParallelHook

AsyncParallelHook 是 异步的并行的钩子,会并发执行所有事件

const { AsyncParallelHook } = require("tapable");
const hook = new AsyncParallelHook(["name", "age", "male"]);

// 注册事件
hook.tapPromise("event1", (name, age, male) => {
  console.log("event1:", name, age, male);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("event1"); // 无法通过 resolve 传递参数
    }, 2000);
  });
});

hook.tapAsync("event2", (name, age, male, callback) => {
  console.log("event2:", name, age, male);
  callback(undefined, 'event2的返回值');
});

hook.tapAsync("event3", (name, age, male, callback) => {
  console.log("event3:", name, age, male);
  setTimeout(() => {
    // callback 的第一个参数不为 undefined 时,相当于报错
    // 通过 promise 执行事件的话这个参数值就传给了 catch(err) 的 err
    callback(undefined, 'event3的返回值')
  }, 1000);
});

hook.callAsync("Jolyne", 23, "male", (err, res) => {
  console.log("结束了", err, res);
});

//控制台输出结果
// event1: Jolyne 23 male
// event2: Jolyne 23 male
// event3: Jolyne 23 male
// 结束了 undefined undefined //两秒后输出,因为 event1 的宏任务先进栈

AsyncParallelBailHook

AsyncParallelBailHook 是 异步的并行的保险类的钩子,与同步的类似

const { AsyncParallelBailHook } = require("tapable");

const hook = new AsyncParallelBailHook(["name", "age", "gender"]); 


hook.tapAsync("event1", (name, age, gender, callback) => {
  console.log("event1", name, age, gender);
  setTimeout(() => {
    callback();
  }, 1000);
});

hook.tapAsync("event2", (name, age, gender, callback) => {
  console.log("event2", name, age, gender);
  setTimeout(() => {
    callback(undefined, "event2的返回值"); // 此时相当于返回了不为 undefined 的返回值,直接中断后续事件
  }, 2000);
});

hook.tapAsync("event3", (name, age, gender, callback) => {
  console.log("event3", name, age, gender);
  setTimeout(() => {
    callback(undefined, "event3的返回值");
  }, 3000);
});

hook.callAsync("Jolyne", 23, 'male', (err, result) => {
  //等全部都完成了才会走到这里来
  console.log("结束了", err, result);
});

//控制台输出结果
// event1 Jolyne 23 male
// event2 Jolyne 23 male
// event3 Jolyne 23 male
// 结束了 null event2的返回值 //两秒后打印

🙅‍♂️ 拒绝钓鱼!用拦截器测试她的忠心!

虽然 Tapable 大胆示爱集帅们,但谁知道她只是窥窃集帅们的美色和财力呢!因此在《 Tapable 的求爱指南》中记载了 Tapable 中的每一个钩子,都有拦截器属性,和 Axios 类似,我们可以通过配置拦截器,对每个 hook 执行的流程做出一些额外的操作。对渣女说哒咩!

const { SyncHook } = require("tapable");
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

hook.intercept({
  // 每次调用 hook 实例的 tap() 方法注册回调函数时, 都会调用该方法, 并且接受 tap 作为参数, 还可以对 tap 进行修改;
  register: (tapInfo) => {
    console.log('===========register==========', tapInfo);
    console.log(`${tapInfo.name} is doing its job`); // tapInfo.name就是注册事件时传入的标识符...


    //注册事件的时候,都对 tapInfo.name 修改一下
    tapInfo.name = tapInfo.name + '!!!!!!!'
    return tapInfo;
  },

  // 通过 hook实例对象 上的 call 方法时候触发拦截器
  call: (arg1, arg2, arg3) => {
    console.log("===========call==========", arg1, arg2, arg3);
  },

  // 在调用被注册的每一个事件函数之前执行
  tap: (tapInfo) => {
    console.log("===========tap==========", tapInfo);
  },
  // loop类型 钩子中 每个事件函数被调用前触发该拦截器方法
  loop: (...args) => {
    console.log("===========tap==========", args);
  },
});

// 注册事件
hook.tap("event1", (arg1, arg2, arg3) => {
  console.log("event1被执行了 ", arg1, arg2, arg3);
});

hook.tap("event2", (arg1, arg2, arg3) => {
  console.log("event12被执行了 ", arg1, arg2, arg3);
});

// 调用事件并传递执行参数
hook.call("Joylne", 23, "male");

// 控制台打印如下:
// ===========register========== { type: 'sync', fn: [Function (anonymous)], name: 'event1' }
// event1 is doing its job
// ===========register========== { type: 'sync', fn: [Function (anonymous)], name: 'event2' }  
// event2 is doing its job
// ===========call========== Joylne 23 male
// ===========tap========== { type: 'sync', fn: [Function (anonymous)], name: 'event1!!!!!!!' }
// event1被执行了  Joylne 23 male
// ===========tap========== { type: 'sync', fn: [Function (anonymous)], name: 'event2!!!!!!!' }
// event12被执行了  Joylne 23 male

从上面我们不妨看出拦截器的流程如下:

  • register:通过 tap()tapAsync()tapPromise注册事件后,会触发该拦截器,它接受一个 tapInfo 的参数,参数值是:{ type: 'sync', fn: [Function (anonymous)], name: 'event1' }type 表示同步还是异步、fn 就是注册时的回调函数,name 就是注册时的事件占位符
  • call:触发事件时触发该拦截器,接受 tapInfo 参数,参数值就是调用 call 时传入的值
  • tap:在执行每一个事件的 callback 之前调用

💘 掌握主动!讓taの吢蒾矢

鲁迅曾说过:“在爱情里面没有人愿意被动”。所以集帅们可以在注册事件函数时,第一个参数传入一个对象。 我们可以通过这个对象上的 stagebefore 属性来控制本次注册的事件函数执行时机。掌握主动,让她的心砰砰直跳💟

Before 属性

Before 属性的值可以传入一个数组或者字符串,值为注册事件对象时的名称,它可以修改当前事件函数在传入的事件名称对应的函数之前进行执行。

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

const hooks = new SyncHook();

hooks.tap({ name: "event1" }, () => console.log("event1被执行了"));

hooks.tap(
  {
    name: "event2",
    // event2 会在 event1 前执行
    before: "event1",
  },
  () => console.log("event2被执行了")
);

hooks.call();

// 控制台打印结果
// event2被执行了
// event1被执行了

Stage 属性

Stage 属性的类型是 Number数字越大,事件回调执行的越晚,支持传入负数,默认为0.

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

const hooks = new SyncHook();

hooks.tap({ name: "event1" }, () => console.log("event1被执行了"));

hooks.tap(
  {
    name: "event2",
    // event2 的 stage 是 1 > event1 的 stage 0,所以 event1 先执行
    stage: 1,
  },
  () => console.log("event2被执行了")
);

hooks.call();

// 控制台打印结果
// event1被执行了
// event2被执行了

😭 终成眷属!终于和 Tapable 酱在一起啦!

Tapable酱:”仙贝!请一定要让我幸福啊!“💞