关于javascript:webpack核心模块tapable源码解析

4次阅读

共计 12728 个字符,预计需要花费 32 分钟才能阅读完成。

上一篇文章我写了 tapable 的根本用法,咱们晓得他是一个增强版版的 公布订阅模式 ,本文想来学习下他的源码。tapable 的源码我读了一下,发现他的形象水平比拟高,间接扎进去反而会让人云里雾里的,所以本文会从最简略的 SyncHook公布订阅模式 动手,再一步一步形象,缓缓变成他源码的样子。

本文可运行示例代码曾经上传 GitHub,大家拿下来一边玩一边看文章成果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code

SyncHook的根本实现

上一篇文章曾经讲过 SyncHook 的用法了,我这里就不再开展了,他应用的例子就是这样子:

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

// 实例化一个减速的 hook
const accelerate = new SyncHook(["newSpeed"]);

// 注册第一个回调,减速时记录下以后速度
accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", ` 减速到 ${newSpeed}`)
);

// 再注册一个回调,用来检测是否超速
accelerate.tap("OverspeedPlugin", (newSpeed) => {if (newSpeed > 120) {console.log("OverspeedPlugin", "您已超速!!");
  }
});

// 触发一下减速事件,看看成果吧
accelerate.call(500);

其实这种用法就是一个最根本的 公布订阅模式,我之前讲公布订阅模式的文章讲过,咱们能够仿照那个很快实现一个SyncHook

class SyncHook {constructor(args = []) {
        this._args = args;       // 接管的参数存下来
        this.taps = [];          // 一个存回调的数组}

    // tap 实例办法用来注册回调
    tap(name, fn) {
        // 逻辑很简略,间接保留下传入的回调参数就行
        this.taps.push(fn);
    }

    // call 实例办法用来触发事件,执行所有回调
    call(...args) {
        // 逻辑也很简略,将注册的回调一个一个拿进去执行就行
        const tapsLength = this.taps.length;
        for(let i = 0; i < tapsLength; i++) {const fn = this.taps[i];
            fn(...args);
        }
    }
}

这段代码非常简单,是一个最根底的 公布订阅模式 ,应用办法跟下面是一样的,将SyncHooktapable导出改为应用咱们本人的:

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

运行成果是一样的:

留神: 咱们构造函数外面传入的 args 并没有用上,tapable次要是用它来动静生成 call 的函数体的,在前面讲代码工厂的时候会看到。

SyncBailHook的根本实现

再来一个 SyncBailHook 的根本实现吧,SyncBailHook的作用是以后一个回调返回不为 undefined 的值的时候,阻止前面的回调执行。根本应用是这样的:

const {SyncBailHook} = require("tapable");    // 应用的是 SyncBailHook

const accelerate = new SyncBailHook(["newSpeed"]);

accelerate.tap("LoggerPlugin", (newSpeed) =>
  console.log("LoggerPlugin", ` 减速到 ${newSpeed}`)
);

// 再注册一个回调,用来检测是否超速
// 如果超速就返回一个谬误
accelerate.tap("OverspeedPlugin", (newSpeed) => {if (newSpeed > 120) {console.log("OverspeedPlugin", "您已超速!!");

    return new Error('您已超速!!');
  }
});

// 因为上一个回调返回了一个不为 undefined 的值
// 这个回调不会再运行了
accelerate.tap("DamagePlugin", (newSpeed) => {if (newSpeed > 300) {console.log("DamagePlugin", "速度切实太快,车子快散架了。。。");
  }
});

accelerate.call(500);

他的实现跟下面的 SyncHook 也十分像,只是 call 在执行的时候不一样而已,SyncBailHook须要检测每个回调的返回值,如果不为 undefined 就终止执行前面的回调,所以代码实现如下:

class SyncBailHook {constructor(args = []) {
        this._args = args;       
        this.taps = [];}

    tap(name, fn) {this.taps.push(fn);
    }

    // 其余代码跟 SyncHook 是一样的,就是 call 的实现不一样
    // 须要检测每个返回值,如果不为 undefined 就终止执行
    call(...args) {
        const tapsLength = this.taps.length;
        for(let i = 0; i < tapsLength; i++) {const fn = this.taps[i];
            const res = fn(...args);

            if(res !== undefined) return res;
        }
    }
}

而后改下 SyncBailHook 从咱们本人的引入就行:

// const {SyncBailHook} = require("tapable"); 
const {SyncBailHook} = require("./SyncBailHook"); 

运行成果是一样的:

形象反复代码

当初咱们只实现了 SyncHookSyncBailHook两个 Hook 而已,上一篇讲用法的文章外面总共有 9 个 Hook,如果每个Hook 都像后面这样实现也是能够的。然而咱们再认真看下 SyncHookSyncBailHook两个类的代码,发现他们除了 call 的实现不一样,其余代码截然不同,所以作为一个有谋求的工程师,咱们能够把这部分反复的代码提出来作为一个基类:Hook类。

Hook类须要蕴含一些公共的代码,call这种不一样的局部由各个子类本人实现。所以 Hook 类就长这样:

const CALL_DELEGATE = function(...args) {this.call = this._createCall();
    return this.call(...args);
};

// Hook 是 SyncHook 和 SyncBailHook 的基类
// 大体构造是一样的,不一样的中央是 call
// 不同子类的 call 是不一样的
// tapable 的 Hook 基类提供了一个形象接口 compile 来动静生成 call 函数
class Hook {constructor(args = []) {
        this._args = args;       
        this.taps = [];          

        // 基类的 call 初始化为 CALL_DELEGATE
        // 为什么这里须要这样一个代理,而不是间接 this.call = _createCall()
        // 等咱们后体面类实现了再一起讲
        this.call = CALL_DELEGATE;
    }

    // 一个形象接口 compile
    // 由子类实现,基类 compile 不能间接调用
    compile(options) {throw new Error("Abstract: should be overridden");
    }

    tap(name, fn) {this.taps.push(fn);
    }

    // _createCall 调用子类实现的 compile 来生成 call 办法
    _createCall() {
      return this.compile({
        taps: this.taps,
        args: this._args,
      });
    }
}

官网对应的源码看这里:https://github.com/webpack/tapable/blob/master/lib/Hook.js

子类 SyncHook 实现

当初有了 Hook 基类,咱们的 SyncHook 就须要继承这个基类重写,tapable在这里继承的时候并没有应用class extends,而是手动继承的:

const Hook = require('./Hook');

function SyncHook(args = []) {
    // 先手动继承 Hook
      const hook = new Hook(args);
    hook.constructor = SyncHook;

    // 而后实现本人的 compile 函数
    // compile 的作用应该是创立一个 call 函数并返回
        hook.compile = function(options) {
        // 这里 call 函数的实现跟后面实现是一样的
        const {taps} = options;
        const call = function(...args) {
            const tapsLength = taps.length;
            for(let i = 0; i < tapsLength; i++) {const fn = this.taps[i];
                fn(...args);
            }
        }

        return call;
    };
    
    return hook;
}

SyncHook.prototype = null;

留神 :咱们在基类Hook 构造函数中初始化 this.callCALL_DELEGATE这个函数,这是有起因的,最次要的起因是 确保 this 的正确指向 。思考一下如果咱们不必CALL_DELEGATE,而是间接this.call = this._createCall() 会产生什么?咱们来剖析下这个执行流程:

  1. 用户应用时,必定是应用new SyncHook(),这时候会执行const hook = new Hook(args);
  2. new Hook(args)会去执行 Hook 的构造函数,也就是会运行this.call = this._createCall()
  3. 这时候的 this 指向的是基类 Hook 的实例,this._createCall()会调用基类的this.compile()
  4. 因为基类的 complie 函数是一个形象接口,间接调用会报错Abstract: should be overridden

那咱们采纳 this.call = CALL_DELEGATE 是怎么解决这个问题的呢

  1. 采纳 this.call = CALL_DELEGATE 后,基类 Hook 上的 call 就只是被赋值为一个代理函数而已,这个函数不会立马调用。
  2. 用户应用时,同样是 new SyncHook(),外面会执行Hook 的构造函数
  3. Hook构造函数会给 this.call 赋值为CALL_DELEGATE,然而不会立刻执行。
  4. new SyncHook()继续执行,新建的实例上的办法 hook.complie 被覆写为正确办法。
  5. 当用户调用 hook.call 的时候才会真正执行this._createCall(),这外面会去调用this.complie()
  6. 这时候调用的 complie 曾经是被正确覆写过的了,所以失去正确的后果。

子类 SyncBailHook 的实现

子类 SyncBailHook 的实现跟下面 SyncHook 的也是十分像,只是 hook.compile 实现不一样而已:

const Hook = require('./Hook');

function SyncBailHook(args = []) {
    // 根本构造跟 SyncHook 都是一样的
      const hook = new Hook(args);
    hook.constructor = SyncBailHook;

    
    // 只是 compile 的实现是 Bail 版的
        hook.compile = function(options) {const { taps} = options;
        const call = function(...args) {
            const tapsLength = taps.length;
            for(let i = 0; i < tapsLength; i++) {const fn = this.taps[i];
                const res = fn(...args);

                if(res !== undefined) break;
            }
        }

        return call;
    };
    
    return hook;
}

SyncBailHook.prototype = null;

形象代码工厂

下面咱们通过对 SyncHookSyncBailHook的形象提炼出了一个基类 Hook,缩小了反复代码。基于这种构造子类须要实现的就是complie 办法,然而如果咱们将 SyncHookSyncBailHookcomplie 办法拿进去比照下:

SyncHook:

hook.compile = function(options) {const { taps} = options;
  const call = function(...args) {
    const tapsLength = taps.length;
    for(let i = 0; i < tapsLength; i++) {const fn = this.taps[i];
      fn(...args);
    }
  }

  return call;
};

SyncBailHook

hook.compile = function(options) {const { taps} = options;
  const call = function(...args) {
    const tapsLength = taps.length;
    for(let i = 0; i < tapsLength; i++) {const fn = this.taps[i];
      const res = fn(...args);

      if(res !== undefined) return res;
    }
  }

  return call;
};

咱们发现这两个 complie 也十分像,有大量反复代码,所以 tapable 为了解决这些反复代码,又进行了一次形象,也就是代码工厂 HookCodeFactoryHookCodeFactory 的作用就是用来生成 complie 返回的 call 函数体,而 HookCodeFactory 在实现时也采纳了 Hook 相似的思路,也是先实现了一个基类 HookCodeFactory,而后不同的Hook 再继承这个类来实现本人的代码工厂,比方SyncHookCodeFactory

创立函数的办法

在持续深刻代码工厂前,咱们先来回顾下 JS 外面创立函数的办法。个别咱们会有这几种办法:

  1. 函数申明

    function add(a, b) {return a + b;}
  2. 函数表达式

    const add = function(a, b) {return a + b;}

然而除了这两种办法外,还有种不罕用的办法:应用 Function 构造函数。比方下面这个函数应用构造函数创立就是这样的:

const add = new Function('a', 'b', 'return a + b;');

下面的调用模式里,最初一个参数是函数的函数体,后面的参数都是函数的形参,最终生成的函数跟用函数表达式的成果是一样的,能够这样调用:

add(1, 2);    // 后果是 3 

留神 :下面的ab形参放在一起用逗号隔开也是能够的:

const add = new Function('a, b', 'return a + b;');    // 这样跟下面的成果是一样的

当然函数并不是肯定要有参数,没有参数的函数也能够这样创立:

const sayHi = new Function('alert("Hello")');

sayHi(); // Hello

这样创立函数和后面的函数申明和函数表达式有什么区别呢?应用 Function 构造函数来创立函数最大的一个特色就是,函数体是一个字符串,也就是说咱们能够动静生成这个字符串,从而动静生成函数体 。因为SyncHookSyncBailHookcall 函数很像,咱们能够像拼一个字符串那样拼出他们的函数体,为了更简略的拼凑,tapable最终生成的 call 函数外面并没有循环,而是在拼函数体的时候就将循环展开了,比方 SyncHook 拼出来的 call 函数的函数体就是这样的:

"use strict";
var _x = this._x;
var _fn0 = _x[0];
_fn0(newSpeed);
var _fn1 = _x[1];
_fn1(newSpeed);

下面代码的 _x 其实就是保留回调的数组 taps,这里重命名为_x,我想是为了节俭代码大小吧。这段代码能够看到,_x,也就是taps 外面的内容曾经被开展了,是一个一个取出来执行的。

SyncBailHook 最终生成的 call 函数体是这样的:

"use strict";
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(newSpeed);
if (_result0 !== undefined) {
    return _result0;
    ;
} else {var _fn1 = _x[1];
    var _result1 = _fn1(newSpeed);
    if (_result1 !== undefined) {
        return _result1;
        ;
    } else {}}

这段生成的代码主体逻辑其实跟 SyncHook 是一样的,都是将 _x 开展执行了,他们的区别是 SyncBailHook 会对每次执行的后果进行检测,如果后果不是 undefined 就间接 return 了,前面的回调函数就没有机会执行了。

创立代码工厂基类

基于这个目标,咱们的代码工厂基类应该能够生成最根本的 call 函数体。咱们来写个最根本的 HookCodeFactory 吧,目前他只能生成 SyncHookcall函数体:

class HookCodeFactory {constructor() {
        // 构造函数定义两个变量
        this.options = undefined;
        this._args = undefined;
    }

    // init 函数初始化变量
    init(options) {
        this.options = options;
        this._args = options.args.slice();}

    // deinit 重置变量
    deinit() {
        this.options = undefined;
        this._args = undefined;
    }

    // args 用来将传入的数组 args 转换为 New Function 接管的逗号分隔的模式
    // ['arg1', 'args'] --->  'arg1, arg2'
    args() {return this._args.join(",");
    }

    // setup 其实就是给生成代码的_x 赋值
    setup(instance, options) {instance._x = options.taps.map(t => t);
    }

    // create 创立最终的 call 函数
    create(options) {this.init(options);
        let fn;

        // 间接将 taps 开展为平铺的函数调用
        const {taps} = options;
        let code = '';
        for (let i = 0; i < taps.length; i++) {
            code += `
                var _fn${i} = _x[${i}];
                _fn${i}(${this.args()});
            `
        }

        // 将开展的循环和头部连接起来
        const allCodes = `
            "use strict";
            var _x = this._x;
        ` + code;

        // 用传进来的参数和生成的函数体创立一个函数进去
        fn = new Function(this.args(), allCodes);

        this.deinit();  // 重置变量

        return fn;    // 返回生成的函数
    }
}

下面代码最外围的其实就是 create 函数,这个函数会动态创建一个 call 函数并返回,所以 SyncHook 能够间接应用这个 factory 创立代码了:

// SyncHook.js

const Hook = require('./Hook');
const HookCodeFactory = require("./HookCodeFactory");

const factory = new HookCodeFactory();

// COMPILE 函数会去调用 factory 来生成 call 函数
const COMPILE = function(options) {factory.setup(this, options);
    return factory.create(options);
};

function SyncHook(args = []) {const hook = new Hook(args);
    hook.constructor = SyncHook;

    // 应用 HookCodeFactory 来创立最终的 call 函数
    hook.compile = COMPILE;

    return hook;
}

SyncHook.prototype = null;

让代码工厂反对SyncBailHook

当初咱们的 HookCodeFactory 只能生成最简略的 SyncHook 代码,咱们须要对他进行一些改良,让他可能也生成 SyncBailHookcall函数体。你能够拉回后面再仔细观察下这两个最终生成代码的区别:

  1. SyncBailHook须要对每次执行的 result 进行解决,如果不为 undefined 就返回
  2. SyncBailHook生成的代码其实是 if...else 嵌套的,咱们生成的时候能够思考应用一个递归函数

为了让 SyncHookSyncBailHook的子类代码工厂可能传入差异化的 result 解决,咱们先将 HookCodeFactory 基类的 create 拆成两局部,将代码拼装的逻辑独自拆成一个函数:

class HookCodeFactory {
    // ...
      // 省略其余一样的代码
      // ...

    // create 创立最终的 call 函数
    create(options) {this.init(options);
        let fn;

        // 拼装代码头部
        const header = `
            "use strict";
            var _x = this._x;
        `;

        // 用传进来的参数和函数体创立一个函数进去
        fn = new Function(this.args(),
            header +
            this.content());         // 留神这里的 content 函数并没有在基类 HookCodeFactory 实现,而是子类实现的

        this.deinit();

        return fn;
    }

    // 拼装函数体
      // callTapsSeries 也没在基类调用,而是子类调用的
    callTapsSeries() {const { taps} = this.options;
        let code = '';
        for (let i = 0; i < taps.length; i++) {
            code += `
                var _fn${i} = _x[${i}];
                _fn${i}(${this.args()});
            `
        }

        return code;
    }
}

下面代码外面要特地留神 create 函数外面生成函数体的时候调用的是 this.content,然而this.content 并没与在基类实现,这要求子类在应用 HookCodeFactory 的时候都须要继承他并实现本人的 content 函数,所以这里的 content 函数也是一个形象接口。那 SyncHook 的代码就应该改成这样:

// SyncHook.js

// ... 省略其余一样的代码 ...

// SyncHookCodeFactory 继承 HookCodeFactory 并实现 content 函数
class SyncHookCodeFactory extends HookCodeFactory {content() {return this.callTapsSeries();    // 这里的 callTapsSeries 是基类的
    }
}

// 应用 SyncHookCodeFactory 来创立 factory
const factory = new SyncHookCodeFactory();

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

留神这里:子类实现的 content 其实又调用了基类的 callTapsSeries 来生成最终的函数体。所以这里这几个函数的调用关系其实是这样的:

那这样设计的目标是什么呢 为了让子类 content 可能传递参数给基类 callTapsSeries,从而生成不一样的函数体。咱们马上就能在SyncBailHook 的代码工厂上看到了。

为了可能生成 SyncBailHook 的函数体,咱们须要让 callTapsSeries 反对一个 onResult 参数,就是这样:

class HookCodeFactory {
    // ... 省略其余雷同的代码 ...

    // 拼装函数体,须要反对 options.onResult 参数
    callTapsSeries(options) {const { taps} = this.options;
        let code = '';
        let i = 0;

        const onResult = options && options.onResult;
        
        // 写一个 next 函数来开启有 onResult 回调的函数体生成
        // next 和 onResult 互相递归调用来生成最终的函数体
        const next = () => {if(i >= taps.length) return '';

            const result = `_result${i}`;
            const code = `
                var _fn${i} = _x[${i}];
                var ${result} = _fn${i}(${this.args()});
                ${onResult(i++, result, next)}
            `;

            return code;
        }

        // 反对 onResult 参数
        if(onResult) {code = next();
        } else {
              // 没有 onResult 参数的时候,即 SyncHook 跟之前放弃一样
            for(; i< taps.length; i++) {
                code += `
                    var _fn${i} = _x[${i}];
                    _fn${i}(${this.args()});
                `
            }
        }

        return code;
    }
}

而后咱们的 SyncBailHook 的代码工厂在继承工厂基类的时候须要传一个 onResult 参数,就是这样:

const Hook = require('./Hook');
const HookCodeFactory = require("./HookCodeFactory");

// SyncBailHookCodeFactory 继承 HookCodeFactory 并实现 content 函数
// content 外面传入定制的 onResult 函数,onResult 回去调用 next 递归生成嵌套的 if...else...
class SyncBailHookCodeFactory extends HookCodeFactory {content() {
        return this.callTapsSeries({onResult: (i, result, next) =>
                `if(${result} !== undefined) {\nreturn ${result};\n} else {\n${next()}}\n`,
        });
    }
}

// 应用 SyncHookCodeFactory 来创立 factory
const factory = new SyncBailHookCodeFactory();

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


function SyncBailHook(args = []) {
    // 根本构造跟 SyncHook 都是一样的
    const hook = new Hook(args);
    hook.constructor = SyncBailHook;

    // 应用 HookCodeFactory 来创立最终的 call 函数
    hook.compile = COMPILE;

    return hook;
}

当初运行下代码,成果跟之前一样的,功败垂成~

其余 Hook 的实现

到这里,tapable的源码架构和根本实现咱们曾经弄清楚了,然而本文只用了 SyncHookSyncBailHook做例子,其余的,比方 AsyncParallelHook 并没有开展讲。因为 AsyncParallelHook 之类的其余 Hook 的实现思路跟本文是一样的,比方咱们能够先实现一个独立的 AsyncParallelHook 类:

class AsyncParallelHook {constructor(args = []) {
        this._args = args;
        this.taps = [];}
    tapAsync(name, task) {this.taps.push(task);
    }
    callAsync(...args) {
        // 先取出最初传入的回调函数
        let finalCallback = args.pop();

        // 定义一个 i 变量和 done 函数,每次执行检测 i 值和队列长度,决定是否执行 callAsync 的最终回调函数
        let i = 0;
        let done = () => {if (++i === this.taps.length) {finalCallback();
            }
        };

        // 顺次执行事件处理函数
        this.taps.forEach(task => task(...args, done));
    }
}

而后对他的 callAsync 函数进行形象,将其形象到代码工厂类外面,应用字符串拼接的形式动静结构进去就行了,整体思路跟后面是一样的。具体实现过程能够参考 tapable 源码:

Hook 类源码

SyncHook 类源码

SyncBailHook 类源码

HookCodeFactory 类源码

总结

本文可运行示例代码曾经上传 GitHub,大家拿下来一边玩一边看文章成果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code

上面再对本文的思路进行一个总结:

  1. tapable的各种 Hook 其实都是基于公布订阅模式。
  2. 各个 Hook 本人独立实现其实也没有问题,然而因为都是公布订阅模式,会有大量反复代码,所以 tapable 进行了几次形象。
  3. 第一次形象是提取一个 Hook 基类,这个基类实现了初始化和事件注册等公共局部,至于每个 Hookcall都不一样,须要本人实现。
  4. 第二次形象是每个 Hook 在实现本人的 call 的时候,发现代码也有很多相似之处,所以提取了一个代码工厂,用来动静生成 call 的函数体。
  5. 总体来说,tapable的代码并不难,然而因为有两次形象,整个代码架构显得不那么好读,通过本文的梳理后,应该会好很多了。

文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。

欢送关注我的公众号进击的大前端第一工夫获取高质量原创~

“前端进阶常识”系列文章源码地址:https://github.com/dennis-jiang/Front-End-Knowledges

参考资料

tapable用法介绍:https://segmentfault.com/a/1190000039418800

tapable源码地址:https://github.com/webpack/tapable

正文完
 0