likes
comments
collection
share

偷师tapable三个技术点

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

杰克-逊の黑豹,恰饭了啦 []( ̄▽ ̄)

因何而起

记得上次找工作的时候,我再次重温被webpack八股文虐过的日子。以至于到后来成功入职,我对 webpack 仍心有余悸。实际上我准备了webpack方面的内容,结果面试的时候没问😅。

虽侥幸逃过一劫,但入职之后私下里还是复盘了下webpack源码。其中webpackhook方面没有自己独立实现,而是依赖了一个叫做 tapable 的第三方库。我顿时感到惊奇。webpack这么牛皮的库竟然也有dependency?

于是乎,我拜读了tapable源码,发现了三个技术点,今天分享给大家。

骚到起飞的Function

以前我们要想封装一套逻辑,就会去定义一个函数,比如下面:

function sayHello(name: string) {
   if (name === "杰克-逊の黑豹") {
         console.log("hello Michael");
   } else {
         console.log("我才不和你hello呢");
   }
}

注意,这个函数是静态定义好的,后续的函数体不会发生变化。换句话说,函数体的内容你其实早就知道了, 是固定的。

如果是下面这个情形,该怎么办呢。

let isMichael = true;

function sayHello(name: string) {
   // 我希望在代码运行期间,如果 isMichael = true,
   // 函数体的内容是这样:
   //
   // if (name === "杰克-逊の黑豹") {
   //      console.log("hello Michael");
   // } else {
   //       console.log("我才不和你hello呢");
   // }
   
   // 如果 isMichael = false, 函数体的内容是这样:
   // console.log("杰克-逊の黑豹 say hello to you, " + name);
}

tapable就使出了Function

说实话,平常开发都和具体的函数打交道,谁会想到Function ? 除了原型链八股文中会扯上Function,其他场景我还真是孤陋寡闻了。

偷师tapable三个技术点

上图是tapable的一个截图,简单的说,Function就是这么用的:

let sayHello = new Function("name", 
             "if (name === '杰克-逊の黑豹') { \n" +
             "      console.log('hello Michael'); \n" +
             "} else { \n" +
             "      console.log('我才不和你hello呢'); \n" +
             "}");
             
// 相当于定义了上文描述的sayHello函数

// “name” 就是函数参数名, 
// 如果有多个参数,可以这样定义:
//    new Function("name1", "name2", "...nameRest", "console.log(name1)")
//    new Function("name1,name2, ...nameRest", "console.log(name1)");
//
// 如果没有参数,可以这样定义:
//    new Function("console.log('hello Michael')")

// Function的第二个参数就是函数体,一个字符串,注意哈,这直接表示的就是函数体内容,函数体开始
// 和结束的 “{”   "}"  不用写!

// 我们也可以在函数体内使用 arguments 变量

再来简单说下tapable中的场景: tapable将一个事件源定义为一个hook,在hook上可以注册事件和回调函数,tapable称之为注册一个tap,当hook触发的时候,这些被注册的tap就会被执行。

按照这种逻辑,我们会把tap存储到一个队列中,然后依次取出来,执行回调函数就可以了。

但是tapable的做法是将回调函数取出来,然后用Function调用封装在一起,再去执行这个函数。

hhhh, 可能webpack的慢有一些道理在这里?

可问题来了,回调函数似乎没办法序列化成字符串,然后集成到Function中耶。

该怎么办?该怎么办呢?(坏笑)

这就是Function比较骚的地方了。

Function函数体部分是在全局范围内捕捉变量的。

请看:

let sayHello =  new Function("console.log('hello, ', this.name)");
let peter = {
     name: "Peter",
     // this.name 中的 this 说的是 peter 对象
     sayHello,
};

peter.sayHello();

// “hello, Peter”

let tim = {
     name: "Tim",
     sayHello,
};

tim.sayHello();

// "hello, Tim"

怎么用呢?

我们虽然不能把回调函数自身变成字符串塞进Function,但是在存储的时候,我们知道回调函数存储在了哪个队列变量中,在队列里的下标是多少,那么这个队列变量就像例子中的peter.name一样,可以嵌入到Function中,间接地将回调函数接入到Function中了。

这种操作,我觉得挺神奇的。

总有惊喜的constructor

tapable有严密的面向对象设计格式,而不是函数式。不过呢,它没有严格去使用 extends 去完成继承。而是使用改写 constructor的方式。

比如:

偷师tapable三个技术点

  • hook是由Hook产生的,但是却将constructor重定向到 SyncHook, Hook是父类,SyncHook是子类;
  • SyncHook.prototype设置为null,这样可以避免hook顺着原型链访问到不必要的方法;
  • 在这种设计下,想给hook暴露什么方法,直接将方法赋值给 hook 即可,不用玩原型链;

这种设计其实挺好的,取我所需,无需则我分文不取

有个地方混淆到我了。

原本我以为constructor只存在于函数的prototype属性上,比如Hook.prototype.constructor.

而实例化的对象上也有该属性,比如(new Hook()).constructor

常见套路——子类覆写方法

看看父类的Hook comiple方法: 偷师tapable三个技术点

直接在父类的compile方法故意抛错,强制子类去覆盖此方法,这是在拿运行时来保证,有点狠啊。

除此之外,在 python 的一些框架中也有这样的设计处理,可能就是一种行业默认的套路吧。

结语

这只是能用得上的小技术点而已,对于tapable而言,其整体编码架构才更值得称叹,结构非常有条理,耦合解开得也很棒,扩展性特别棒。就是读起代码,想梳理清楚,不是那么轻松。

你遇见过什么很骚的操作嘛,欢迎分享。