一、前语

本文是 从零到亿系统性的树立前端构建知识体系✨ 中的第六篇,全体难度 ⭐️⭐️⭐️。

回应标题:为什么我主张你一定要读一读 Tapable 源码?

一切人都知道 Webpack 很复杂,但 Webpack 的源码却很高雅,是一个典型的可插拔架构,不只逻辑明晰,并且灵敏好扩展。近几年出来的一些构建工具,大多也都参阅了 Webpack 的这种架构办法。

而完结这一切的中心便是凭借了Tapable。

关于 Tapable 的源码其实并没有多少代码量,学习它的原理首战之地的一定是能够让你在日常 Webpack Plugin 开发中更称心如意,处理相关问题愈加顺利。

其次,Tapable 的内部以特别奇妙的办法完结了发布订阅形式,这之中会有非常多的知识点:比方懒编译或许叫动态编译,关于类与继承抽象类的面向目标思维以及 this 指向的升华等等…

在我个人看来, Tapable 源代码中的规划原则和完结过程对错常值得每一个前端开发者去阅览的。

回到正文

在本文中咱们将会抛开 Webpack,在第 1 ~ 5 节主要是解说根本原理和运用办法(奈何官方文档实在太粗陋…),第 6 节则会以图文的办法深度剖析 Tapable 的完结原理,了解运用的同学可越过前面几节。

通篇将会选用结论先行、自顶向下的办法进行解说,重视完结思路,重视规划思维,与 Webpack 完全解耦,可定心食用。

文中所涉及到的代码均放到个人 github 仓库中:github.com/noBaldAaa/m…

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

二、Tapable是什么?

Tapable是一个类似于 Node.js 中的 EventEmitter 的库,但它更专注于自界说作业的触发和处理。经过 Tapable 咱们能够注册自界说作业,然后在适当的机遇去履行自界说作业。

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

举个例子:类比到 VueReact 结构中的生命周期函数,它们便是到了固定的时刻节点就履行对应的生命周期,tapable 做的作业就和这个差不多,咱们能够经过它先注册一系列的生命周期函数,然后在适宜的时刻点履行。

概念了解的差不多了,接下往来不断实操一下。初始化项目,安装依靠:

npm init //初始化项目
yarn add tapable -D //安装依靠

安装完项目依靠后,依据以下目录结构来增加对应的目录和文件:

├── node_modules
├── package-lock.json
├── package.json
└── src # 源码目录
     └── syncHookDemo.js

依据官方介绍,tapable 运用起来还是挺简略的,只需三步:

  1. 实例化钩子函数( tapable会暴露出各式各样的 Hook,这儿先以同步钩子Synchook为例)
  2. 注册作业
  3. 触发作业

src/syncHookDemo.js

const SyncHook = require("../my/SyncHook"); //这是一个同步钩子
//第一步:实例化钩子函数,能够在这儿界说形参
const syncHook = new SyncHook(["author", "age"]);
//第二步:注册作业1
syncHook.tap("监听器1", (name, age) => {
  console.log("监听器1:", name, age);
});
//第二步:注册作业2
syncHook.tap("监听器2", (name) => {
  console.log("监听器2", name);
});
//第三步:注册作业3
syncHook.tap("监听器3", (name) => {
  console.log("监听器3", name);
});
//第三步:触发作业,这儿传的是实参,会被每一个注册函数接纳到
syncHook.call("不要秃头啊", "99");

运转 node ./src/syncHookDemo.js,拿到履行成果:

监听器1 不要秃头啊 99
监听器2 不要秃头啊
监听器3 不要秃头啊

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

从上面的例子中能够看出 tapable 选用的是发布订阅形式经过 tap 函数注册监听函数,然后经过 call 函数按顺序履行之前注册的函数

大致原理(真实源码中并不是这样,第六节会剖析源码中的完结,这儿协助咱们了解):

class SyncHook {
  constructor() {
    this.taps = [];
  }
  //注册监听函数,这儿的name其实没啥用
  tap(name, fn) {
    this.taps.push({ name, fn });
  }
  //履行函数
  call(...args) {
    this.taps.forEach((tap) => tap.fn(...args));
  }
}

三、依照同步/异步分类

别的,tapable 中不只需 Synchook,还有其他 八个 Hook :

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

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

在这九个 Hook 中所注册的作业能够分为同步、异步两种履行办法,正如称号表述的那样:

  • 同步表明注册的作业函数会同步进行履行
  • 异步表明注册的作业函数会异步进行履行

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

对同步钩子来说, tap 办法是仅有注册作业的办法,经过 call 办法触发同步钩子的履行。

对异步钩子来说,能够经过 taptapAsynctapPromise三种办法来注册,经过对应的 callAsyncpromise 这两种办法来触发注册的函数。

一起异步钩子中还能够分为两类:

  • 异步串行钩子( AsyncSeries ):能够被串联(接连依照顺序调用)履行的异步钩子函数。
  • 异步并行钩子( AsyncParallel ):能够被并联(并发调用)履行的异步钩子函数。

尽管这儿分类分来分去,可是其实咱们能够不用死记硬背,需求用到的时分查文档就好。

四、依照履行机制分类

Tapable 一起也能够依照履行机制进行分类,这儿说一下这几个类型的概念,后边会经过事例细讲:

  • Basic Hook : 根本类型的钩子,履行每一个注册的作业函数,并不关怀每个被调用的作业函数回来值怎么。

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

  • Waterfall : 瀑布类型的钩子,假如前一个作业函数的成果result !== undefined,则 result 会作为后一个作业函数的第一个参数(也便是上一个函数的履行成果会成为下一个函数的参数)

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

  • Bail : 稳妥类型钩子,履行每一个作业函数,遇到第一个成果result !== undefined则回来,不再继续履行(也便是只需其间一个有成果了,后边的就不履行了)

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

  • Loop : 循环类型钩子,不断的循环履行作业函数,直到一切函数成果result === undefined(有点像咱们小时分打单机游戏相同,只需哪一关不小心死了,就得从头再来一遍,直到一切的关卡都打过才算通关)。

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

在最开端的事例中咱们用的SyncHook,它便是一个同步的钩子。又因为并不关怀回来值,所以也算是一个根本类型的 Hook

五、根本运用

5.1、SyncHook

最初所用的事例便是基于 SyncHook,就不再赘述。

5.2、SyncBailHook

SyncBailHook 是一个同步的、稳妥类型的 Hook,意思是只需其间一个有回来了,后边的就不履行了。

src/syncBailHookDemo.js

const { SyncBailHook } = require("tapable");
const hook = new SyncBailHook(["author", "age"]); //先实例化,并界说回调函数的形参
//经过tap函数注册作业
hook.tap("测验1", (param1, param2) => {
  console.log("测验1接纳的参数:", param1, param2);
});
//该监听函数有回来值
hook.tap("测验2", (param1, param2) => {
  console.log("测验2接纳的参数:", param1, param2);
  return "123";
});
hook.tap("测验3", (param1, param2) => {
  console.log("测验3接纳的参数:", param1, param2);
});
//经过call办法触发作业
hook.call("不要秃头啊", "99");

运转 node ./src/syncBailHookDemo.js,拿到履行成果:

测验1接纳的参数: 不要秃头啊 99
测验2接纳的参数: 不要秃头啊 99

5.3、SyncWaterfallHook

SyncWaterfallHook 是一个同步的、瀑布式类型的 Hook。瀑布类型的钩子便是假如前一个作业函数的成果result !== undefined,则 result 会作为后一个作业函数的第一个参数(也便是上一个函数的履行成果会成为下一个函数的参数)

src/syncWaterfallHookDemo.js

const { SyncWaterfallHook } = require("tapable");
const hook = new SyncWaterfallHook(["author", "age"]); //先实例化,并界说回调函数的形参
//经过tap函数注册作业
hook.tap("测验1", (param1, param2) => {
  console.log("测验1接纳的参数:", param1, param2);
});
hook.tap("测验2", (param1, param2) => {
  console.log("测验2接纳的参数:", param1, param2);
  return "123";
});
hook.tap("测验3", (param1, param2) => {
  console.log("测验3接纳的参数:", param1, param2);
});
//经过call办法触发作业
hook.call("不要秃头啊", "99");

运转 node ./src/syncWaterfallHookDemo.js,拿到履行成果:

测验1接纳的参数: 不要秃头啊 99
测验2接纳的参数: 不要秃头啊 99
测验3接纳的参数: 123 99

5.4、SyncLoopHook

SyncLoopHook 是一个同步、循环类型的 Hook。循环类型的意义是不断的循环履行作业函数,直到一切函数成果result === undefined,不符合条件就调头重新开端履行。

src/syncLoopHookDemo.js

const { SyncLoopHook } = require("tapable");
const hook = new SyncLoopHook([]); //先实例化,并界说回调函数的形参
let count = 5;
//经过tap函数注册作业
hook.tap("测验1", () => {
  console.log("测验1里边的count:", count);
  if ([1, 2, 3].includes(count)) {
    return undefined;
  } else {
    count--;
    return "123";
  }
});
hook.tap("测验2", () => {
  console.log("测验2里边的count:", count);
  if ([1, 2].includes(count)) {
    return undefined;
  } else {
    count--;
    return "123";
  }
});
hook.tap("测验3", () => {
  console.log("测验3里边的count:", count);
  if ([1].includes(count)) {
    return undefined;
  } else {
    count--;
    return "123";
  }
});
//经过call办法触发作业
hook.call();

运转 node ./src/syncLoopHookDemo.js,拿到履行成果:

测验1里边的count: 5
测验1里边的count: 4
测验1里边的count: 3
测验2里边的count: 3
测验1里边的count: 2
测验2里边的count: 2
测验3里边的count: 2
测验1里边的count: 1
测验2里边的count: 1
测验3里边的count: 1

5.5、AsyncParallelHook

前面四个都是同步的 Hook,接下来开端看看异步的 Hook

AsyncParallelHook是一个异步并行、根本类型的 Hook,它与同步 Hook 不同的当地在于:

  • 它会一起敞开多个异步使命,并且需求经过 tapAsync 办法来注册作业(同步 Hook 是经过 tap 办法)
  • 在履行注册作业时需求运用 callAsync 办法来触发(同步 Hook 运用的是 call 办法)

一起,在每个注册函数的回调中,会多一个 callback 参数,它是一个函数。履行 callback 函数相当于告知 Hook 它这一个异步使命履行完结了。

src/asyncParallelHookDemo.js

const { AsyncParallelHook } = require("tapable");
const hook = new AsyncParallelHook(["author", "age"]); //先实例化,并界说回调函数的形参
console.time("time");
//异步钩子需求经过tapAsync函数注册作业,一起也会多一个callback参数,履行callback告知hook该注册作业现已履行完结
hook.tapAsync("测验1", (param1, param2, callback) => {
  setTimeout(() => {
    console.log("测验1接纳的参数:", param1, param2);
    callback();
  }, 2000);
});
hook.tapAsync("测验2", (param1, param2, callback) => {
  console.log("测验2接纳的参数:", param1, param2);
  callback();
});
hook.tapAsync("测验3", (param1, param2, callback) => {
  console.log("测验3接纳的参数:", param1, param2);
  callback();
});
//call办法只需同步钩子才有,异步钩子得运用callAsync
hook.callAsync("不要秃头啊", "99", (err, result) => {
  //等全部都完结了才会走到这儿来
  console.log("这是成功后的回调", err, result);
  console.timeEnd("time");
});

运转 node ./src/asyncParallelHookDemo.js,拿到履行成果:

测验2接纳的参数: 不要秃头啊 99
测验3接纳的参数: 不要秃头啊 99
测验1接纳的参数: 不要秃头啊 99
这是成功后的回调 undefined undefined
time: 2.008s

5.6、AsyncParallelBailHook

AsyncParallelBailHook 是一个异步并行、稳妥类型的 Hook,只需其间一个有回来值,就会履行 callAsync 中的回调函数。

src/asyncParallelBailHookDemo.js

const { AsyncParallelBailHook } = require("tapable");
const hook = new AsyncParallelBailHook(["author", "age"]); //先实例化,并界说回调函数的形参
console.time("time");
//异步钩子需求经过tapAsync函数注册作业,一起也会多一个callback参数,履行callback告知hook该注册作业现已履行完结
hook.tapAsync("测验1", (param1, param2, callback) => {
  console.log("测验1接纳的参数:", param1, param2);
  setTimeout(() => {
    callback();
  }, 1000);
});
hook.tapAsync("测验2", (param1, param2, callback) => {
  console.log("测验2接纳的参数:", param1, param2);
  setTimeout(() => {
    callback(null, "测验2有回来值啦");
  }, 2000);
});
hook.tapAsync("测验3", (param1, param2, callback) => {
  console.log("测验3接纳的参数:", param1, param2);
  setTimeout(() => {
    callback(null, "测验3有回来值啦");
  }, 3000);
});
hook.callAsync("不要秃头啊", "99", (err, result) => {
  //等全部都完结了才会走到这儿来
  console.log("这是成功后的回调", result);
  console.timeEnd("time");
});

运转 node ./src/asyncParallelBailHookDemo.js,拿到履行成果:

测验1接纳的参数: 不要秃头啊 99
测验2接纳的参数: 不要秃头啊 99
测验3接纳的参数: 不要秃头啊 99
这是成功后的回调 测验2有回来值啦
time: 2.007s

5.7、AsyncSeriesHook

AsyncSeriesHook 是一个异步、串行类型的 Hook,只需前面的履行完结了,后边的才会一个接一个的履行。

src/asyncSeriesHookDemo.js

const { AsyncSeriesHook } = require("tapable");
const hook = new AsyncSeriesHook(["author", "age"]); //先实例化,并界说回调函数的形参
console.time("time");
//异步钩子需求经过tapAsync函数注册作业,一起也会多一个callback参数,履行callback告知hook该注册作业现已履行完结
hook.tapAsync("测验1", (param1, param2, callback) => {
  console.log("测验1接纳的参数:", param1, param2);
  setTimeout(() => {
    callback();
  }, 1000);
});
hook.tapAsync("测验2", (param1, param2, callback) => {
  console.log("测验2接纳的参数:", param1, param2);
  setTimeout(() => {
    callback();
  }, 2000);
});
hook.tapAsync("测验3", (param1, param2, callback) => {
  console.log("测验3接纳的参数:", param1, param2);
  setTimeout(() => {
    callback();
  }, 3000);
});
hook.callAsync("不要秃头啊", "99", (err, result) => {
  //等全部都完结了才会走到这儿来
  console.log("这是成功后的回调", err, result);
  console.timeEnd("time");
});

运转 node ./src/asyncSeriesHookDemo.js,拿到履行成果:

测验1接纳的参数: 不要秃头啊 99
测验2接纳的参数: 不要秃头啊 99
测验3接纳的参数: 不要秃头啊 99
这是成功后的回调 undefined undefined
time: 6.017s

5.8、AsyncSeriesBailHook

AsyncSeriesBailHook 是一个异步串行、稳妥类型的 Hook。在串行的履行过程中,只需其间一个有回来值,后边的就不会履行了。

src/asyncSeriesBailHookDemo.js

const { AsyncSeriesBailHook } = require("tapable");
const hook = new AsyncSeriesBailHook(["author", "age"]); //先实例化,并界说回调函数的形参
console.time("time");
//异步钩子需求经过tapAsync函数注册作业,一起也会多一个callback参数,履行callback告知hook该注册作业现已履行完结
hook.tapAsync("测验1", (param1, param2, callback) => {
  console.log("测验1接纳的参数:", param1, param2);
  setTimeout(() => {
    callback();
  }, 1000);
});
hook.tapAsync("测验2", (param1, param2, callback) => {
  console.log("测验2接纳的参数:", param1, param2);
  setTimeout(() => {
    callback(null, "123");
  }, 2000);
});
hook.tapAsync("测验3", (param1, param2, callback) => {
  console.log("测验3接纳的参数:", param1, param2);
  setTimeout(() => {
    callback();
  }, 3000);
});
hook.callAsync("不要秃头啊", "99", (err, result) => {
  //等全部都完结了才会走到这儿来
  console.log("这是成功后的回调", result);
  console.timeEnd("time");
});

运转 node ./src/asyncSeriesBailHookDemo.js,拿到履行成果:

测验1接纳的参数: 不要秃头啊 99
测验2接纳的参数: 不要秃头啊 99
这是成功后的回调 123
time: 3.010s

5.9、AsyncSeriesWaterfallHook

AsyncSeriesWaterfallHook 是一个异步串行、瀑布类型的 Hook。假如前一个作业函数的成果result !== undefined,则 result 会作为后一个作业函数的第一个参数(也便是上一个函数的履行成果会成为下一个函数的参数)。

src/asyncSeriesWaterfallHookDemo.js

const { AsyncSeriesWaterfallHook } = require("tapable");
const hook = new AsyncSeriesWaterfallHook(["author", "age"]); //先实例化,并界说回调函数的形参
console.time("time");
//异步钩子需求经过tapAsync函数注册作业,一起也会多一个callback参数,履行callback告知hook该注册作业现已履行完结
hook.tapAsync("测验1", (param1, param2, callback) => {
  console.log("测验1接纳的参数:", param1, param2);
  setTimeout(() => {
    callback(null, "2");
  }, 1000);
});
hook.tapAsync("测验2", (param1, param2, callback) => {
  console.log("测验2接纳的参数:", param1, param2);
  setTimeout(() => {
    callback(null, "3");
  }, 2000);
});
hook.tapAsync("测验3", (param1, param2, callback) => {
  console.log("测验3接纳的参数:", param1, param2);
  setTimeout(() => {
    callback(null, "4");
  }, 3000);
});
hook.callAsync("不要秃头啊", "99", (err, result) => {
  //等全部都完结了才会走到这儿来
  console.log("这是成功后的回调", err, result);
  console.timeEnd("time");
});

运转 node ./src/asyncSeriesWaterfallHookDemo.js,拿到履行成果:

测验1接纳的参数: 不要秃头啊 99
测验2接纳的参数: 2 99
测验3接纳的参数: 3 99
这是成功后的回调 null 4
time: 6.012s

六、具体完结

在开端读源码之前,咱们先得弄清楚怎么调试源码,以及怎么在 IDE 中快速的履行代码文件。

6.1、工欲善其事,必先利其器

(1)调试源码:

第一步:修改 Hook 的引用路径:

syncHookDemo.js

//之前
const { SyncHook } = require("tapable");
//修改后
const SyncHook = require("../node_modules/tapable/lib/SyncHook");

第二步:翻开 Vscode 调试工具,在代码中打上断点:

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

第三步:点击 Run and Debug,选择 Node.js 环境

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

(2)在 IDE 中快速的履行代码文件

第一步:在 IDE 的运用商店中下载插件 Code Runner:

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

第二步:选择想要运转的文件,点击右键,选择 Run Code 选项:

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

第三步:在操控台中查看成果:

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

6.2、中心思维

这儿以 SyncHook 的完结原理为例,其他的 Hook 也会收拾一下思路,咱们触类旁通,要点在于了解思维。

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

咱们再回过头来看看 SyncHook 的用法,也便是这三步:

【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?

中心思维:

其实 tap 函数便是一个搜集器,当调用 tap 函数时需求将传入的这些信息进行搜集,并转换成一个数组,数组里边寄存着注册函数的类型type回调函数(fn)等信息:

this.taps = [
  {
    name: "监听器1",
    type: "sync",
    fn: (param1, param2) => {
      console.log("监听器1接纳参数:", name, age);
    },
  },
  {
    name: "监听器2",
    type: "sync",
    fn: (param1, param2) => {
      console.log("监听器2接纳参数:", name);
    },
  },
]; //用来寄存咱们的回调函数根本信息

调用 call 函数的实质便是 按指定的类型 去履行 this.taps中的注册函数 fn,比方这儿的 type: sync,便是得按同步的办法履行,那咱们只需将运转代码改形成这样:

function anonymous(param1, param2) {
  const taps = this.taps;
  let fn0 = taps[0].fn;
  fn0(param1, param2);
  let fn1 = taps[1].fn;
  fn1(param1, param2);
}
anonymous("不要秃头啊", "99");

假如要依照SyncBailHook(同步、稳妥类型:只需其间一个有回来值,后边的就不履行了 )履行,那咱们只需将运转代码改形成这样:

function anonymous(param1, param2) {
  const taps = this.taps;
  let fn0 = taps[0].fn;
  let result0 = fn0(param1, param2);
  if (result0 !== undefined) {
    return result0;
  } else {
    let fn1 = taps[1].fn;
    let result1 = fn1(param1, param2);
    if (result1 !== undefined) {
      return result1;
    }
  }
}
anonymous("不要秃头啊", "99");

假如得依照 AsyncSeriesHook(异步、串行类型:只需前面的履行完结了,后边的才会一个接一个的履行 )履行,那咱们需求将运转代码改形成这样:

function anonymous(param1, param2, callback) {
  const taps = this.taps;
  let fn0 = taps[0].fn;
  fn0(param1, param2, function (err) {
    if (err) {
      //假如运转过程中报错,则直接结束
      callback(err);
    } else {
      next0();
    }
  });
  function next0() {
    let fn1 = taps[1].fn;
    fn1(param1, param2, function (err) {
      if (err) {
        callback(err);
      } else {
        callback(); //在末尾履行终究的回调函数
      }
    });
  }
}
anonymous("不要秃头啊", "99", (err,result)=>"终究的回调函数");

剩下的类型咱们能够触类旁通,就不类举了。


6.3、怎么生成运转函数

有了可行的思路之后,现在的问题就变成了怎么样生成这些运转函数?

这儿官方的源码中是经过 new Function() 进行创立的,先了解一下 new Function 的语法:

let func = new Function ([arg1, arg2, ...argN], functionBody);
  • arg1, arg2, ... argN(参数称号):是一个有效的 JavaScript 字符串(例如:”a , b”),或许是一个字符串列表(例如:[“a”,”b”])。
  • functionBody(函数体):可履行的JavaScript字符串。

举个例子:

const sum = new Function("a,b", "return a + b");
console.log(sum(2, 6));
//output: 8

这儿咱们能够仔细观察一下上面咱们所需求的目标函数体,以 SyncHook 所需求的函数体为例:

function anonymous(param1, param2) {
  const taps = this.taps;
  let fn0 = taps[0].fn;
  fn0(param1, param2);
  let fn1 = taps[1].fn;
  fn1(param1, param2);
}
anonymous("不要秃头啊", "99");

该函数体其实能够分为两部分:

  • 第一部分(header):获取寄存着注册函数信息的数组 taps
 const taps = this.taps;
  • 第二部分(content):能够经过对 taps 进行遍历生成:
  let fn0 = taps[0].fn;
  fn0(param1, param2);
  let fn1 = taps[1].fn;
  fn1(param1, param2);

现在经过new Function()生成咱们想要的履行函数,就很简略了:

  • 第一步:生成形参字符串("param1 , param2"
  • 第二步:生成函数体中 header 部分
  • 第三步:遍历 taps,生成 content 部分
new Function(this.args().join(","), this.header() + this.content());

中心思路便是这些,接下来咱们就去实操一下!

6.4、手撕代码

依照上面的思路,首要需求经过 tap 函数进行搜集作业,并将搜集到的函数格局化成这样:

this.taps = [
  {
    name: "监听器1",
    type: "sync",
    fn: (param1, param2) => {
      console.log("监听器1接纳参数:", name, age);
    },
  },
  {
    name: "监听器2",
    type: "sync",
    fn: (param1, param2) => {
      console.log("监听器2接纳参数:", name);
    },
  },
]; //用来寄存咱们的回调函数根本信息

大致结构树立:

class SyncHook {
  constructor(args) {
    this.args = Array.isArray(args) ? args : []; //形参列表
    this.taps = []; //这是一个数组,用来寄存注册函数的根本信息
  }
}

这儿界说了两个变量:this.args 用来寄存实例化过程中传入的形参数组this.taps 用来寄存注册函数的根本信息。

(1)taps 的搜集作业

这儿分红两个小步骤,先对传入参数进行格局化。

咱们之前在运用 tap 办法时是这么运用的:

hook.tap("监听器1", callback);

这儿其实是一个语法糖,写完整了是这样:

hook.tap({name:"监听器1",后边还能够有其他参数}, callback);

因此先要做一层格局化处理:

class SyncHook {
  //省掉其他
+ tap(option, fn) {
+   //假如传入的是字符串,包装成目标
+   if (typeof option === "string") {
+     option = { name: option };
+   }
+ }
}

接着界说 tap 函数,搜集注册函数信息:

class SyncHook {
  //省掉其他
  tap(option, fn) {
    //假如传入的是字符串,包装成目标
    if (typeof option === "string") {
      option = { name: option };
    }
+   const tapInfo = { ...option, type: "sync", fn }; //type=sync fn是注册函数
+   this.taps.push(tapInfo);
  }
}

(2)动态生成履行代码

当调用 call 办法时,会走两个要害的步骤:先动态生成履行代码,再履行生成的代码

终究咱们要经过 this.taps 生成如下格局的运转代码:

new Function(
  "param1 , param2",
  `  
  const taps = this.taps;
  let fn0 = taps[0].fn;
  fn0(param1, param2);
  let fn1 = taps[1].fn;
  fn1(param1, param2);
 `
);

这一步需求遍历 this.taps 数组,然后生成对应的函数体字符串,这儿封装成一个函数 compiler 来做:

class SyncHook {
  //省掉其他
+ compile({ args, taps, type }) {
+   const getHeader = () => {
+     let code = "";
+     code += `var taps=this.taps;n`;
+     return code;
+   };
+   const getContent = () => {
+     let code = "";
+     for (let i = 0; i < taps.length; i++) {
+       code += `var fn${i}=taps[${i}].fn;n`;
+       code += `fn${i}(${args.join(",")});n`;
+     }
+     return code;
+   };
+   return new Function(args.join(","), getHeader() + getContent());
  }
}

(3)履行生成的代码

这儿是最终一步,界说 call 办法,然后履行生成的函数体:

class SyncHook {
  //省掉其他
  call(...args) {
    this._call = this.compile({
      taps: this.taps, //tapInfo的数组 [{name,fn,type}]
      args: this.args, //形参数组
      type: "sync",
    }); //动态创立一个call办法 这叫懒编译或许动态编译,最开端没有,用的时分才去创立履行
    return this._call(...args);
  }
}

完整代码在文章最初的 github 链接中。

6.5、为什么这么规划?

看到这儿,估计有不少小伙伴要懵了,为啥这么规划啊?咱们最初讲的完结不是更简略吗?

像这样:

class SyncHook {
  constructor() {
    this.taps = [];
  }
  //注册监听函数,这儿的name其实没啥用
  tap(name, fn) {
    this.taps.push({ name, fn });
  }
  //履行函数
  call(...args) {
    this.taps.forEach((tap) => tap.fn(...args));
  }
}

这么做一部分原因是为了极佳的功能考虑,比方只需在履行 call 办法时才会去动态生成履行函数,假如不履行则不处理(懒编译或许叫动态编译)。

还有一部分原因则是为了愈加灵敏。别忘了,该库里边还有其他类型的 Hook,假如咱们想要完结其他 Hook,只需求界说好各自的 compiler 函数就能够了。

别的,Webpack作者也说到过为什么选用 new Function 的计划,一切都是为了功能考虑,链接在这儿:github.com/webpack/tap… ,有兴趣的能够去看看。

七、总结

本文从一个基础事例出发,先顺次解说了 Tapable 中各种类型 Hook 的根本用法和运转机制,接着再次回到最初的事例中,花了很多篇幅解说 Tapable 的中心思维和完结思路。在这过程中不只讲清楚了怎么去完结,更重要的是授人以渔,剖析了为什么这么做。

最终在手撕代码环节,咱们经过大约40行代码手写了 mini 版的 SyncHook 来加深印象,协助咱们在读源码的过程中会愈加顺利。

全体学习曲线较为平滑,假如文章中有当地呈现纰漏或许认知过错,希望咱们积极指正。

参阅:

  • /post/704098…
  • /post/697457…

八、推荐阅览

  1. 线上崩了?一招教你快速定位问题!
  2. 从零到亿系统性的树立前端构建知识体系✨
  3. 我是怎么带领团队从零到一树立前端标准的?
  4. 【Webpack Plugin】写了个插件跟喜欢的女生表白,成果……
  5. 前端工程化基石 — AST(抽象语法树)以及AST的广泛运用
  6. Webpack深度进阶:两张图完全批注白热更新原理!
  7. 学会这些自界说hooks,让你摸鱼时刻再翻一倍
  8. 浅析前端异常及降级处理
  9. 前端重新部署后,领导跟我说页面崩溃了…
  10. 前端场景下的查找框,你真的了解了吗?

本文正在参加「金石计划 . 瓜分6万现金大奖」