Skip to content
/ mini-tapable Public
  • Notifications
  • Fork 2
  • Star 27
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Sign up for GitHub

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jump to bottom

webpack tapable精讲 #1

Open
lizuncong opened this issue Aug 25, 2022 · 0 comments
Open

webpack tapable精讲 #1

lizuncong opened this issue Aug 25, 2022 · 0 comments

Comments

@lizuncong
Copy link
Owner

lizuncong commented Aug 25, 2022

完整的手写源码仓库

tapablewebpack 插件机制核心。 mini-tapable 不仅解读官方 tapable 的源码,还用自己的思路去实现一遍,并且和官方的运行时间做了个比较,我和webpack作者相关的讨论可以 点击查看。webpack tapable 源码内部根据 new Function 动态生成函数执行体这种优化方式不一定是好的。当我们熟悉了 tapable 后,就基本搞懂了 webpack plugin 的底层逻辑,再回头看 webpack 源码就轻松很多

目录

  • src目录。这个目录下是手写所有的 tapable hook 的源码,每个 hook 都用自己的思路实现一遍,并且和官方的 hook 执行时间做个对比。

tapable的设计理念:单态、多态及内联缓存

由于在 webpack 打包构建的过程中,会有上千(数量其实是取决于自身业务复杂度)个插件钩子执行,同时同类型的钩子在执行时,函数参数固定,函数体相同,因此 tapable 针对这些业务场景进行了相应的优化。这其中最重要的是运用了 单态性及多态性概念, 内联缓存的原理,也可以看这个 issue。为了达到这个目标,tapable 采用 new Function 动态生成函数执行体的方式,主要逻辑在源码的 HookCodeFactory.js文件中。

如何理解 tapable 的设计理念

思考下面两种实现方法,哪一种执行效率高,哪一种实现方式简洁?

// 方法一:
const callFn = (...tasks) => (...args) => {
   for (const fn of tasks) {
      fn(...args)
    }
}

// 方法二:
const callFn2 = (a, b, c) => (x, y) => {
  a(x, y);
  b(x, y);
  c(x, y);
}

callFncallFn2 的目的都是为了实现将一组方法以相同的参数调用,依次执行。很显然,方法一效率明显更高,并且容易扩展,能支持传入数量不固定的一组方法。但是,如果根据 单态性以及内联缓存的说法,很明显方法二的执行效率更高,同时也存在一个问题,即只支持传入a,b,c三个方法,参数形态也固定,这种方式显然没有方法一灵活,那能不能同时兼顾效率以及灵活性呢?答案是可以的。我们可以借助 new Function 动态生成函数体的方式。

class HookCodeFactory {
  constructor(args) {
    this._argNames = args;
    this.tasks = [];
  }
  tap(task) {
    this.tasks.push(task);
  }
  createCall() {
    let code = "";
    // 注意思考这里是如何拼接参数已经函数执行体的
    const params = this._argNames.join(",");
    for (let i = 0; i < this.tasks.length; i++) {
      code += `
        var callback${i} = this.tasks[${i}];
        callback${i}(${params})
      `;
    }
    return new Function(params, code);
  }
  call(...args) {
    const finalCall = this.createCall();
    // 将函数打印出来,方便观察最终拼接后的结果
    console.log(finalCall);
    return finalCall.apply(this, args);
  }
}

// 构造函数接收的arg数组里面的参数,就是task a、b、c三个函数的参数
const callFn = new HookCodeFactory(["x", "y", "z"]);

const a = (x, y, z) => {
  console.log("task a:", x, y, z);
};

const b = (x, y, z) => {
  console.log("task b:", x, y, z);
};

const c = (x, y, z) => {
  console.log("task c:", x, y, z);
};

callFn.tap(a);
callFn.tap(b);
callFn.tap(c);

callFn.call(4, 5, 6);

当我们在浏览器控制台执行上述代码时:

image.png
拼接后的完整函数执行体:

image.png

可以看到,通过这种动态生成函数执行体的方式,我们能够同时兼顾性能及灵活性。我们可以通过 tap 方法添加任意数量的任务,同时通过在初始化构造函数时 new HookCodeFactory(['x', 'y', ..., 'n']) 传入任意参数。

实际上,这正是官方 tapable 的 HookCodeFactory.js的简化版本。这是 tapable 的精华所在。

tapable源码解读

tapable 最主要的源码在 Hook.js 以及 HookCodeFactory.js中。Hook.js 主要是提供了 taptapAsynctapPromise等方法,每个 Hook 都在构造函数内部调用 const hook = new Hook()初始化 hook 实例。HookCodeFactory.js 主要是根据 new Function 动态生成函数执行体。

demo

SyncHook.js 为例,SyncHook 钩子使用如下:

const { SyncHook } = require("tapable");
debugger;
const testhook = new SyncHook(["compilation", "name"]);
// 注册 plugin1
testhook.tap("plugin1", (compilation, name) => {
  console.log("plugin1", name);
  compilation.sum = compilation.sum + 1;
});

// 注册 plugin2
testhook.tap("plugin2", (compilation, name) => {
  console.log("plugin2..", name);
  compilation.sum = compilation.sum + 2;
});

// 注册 plugin3
testhook.tap("plugin3", (compilation, name) => {
  console.log("plugin3", compilation, name);
  compilation.sum = compilation.sum + 3;
});

const compilation = { sum: 0 };
// 第一次调用
testhook.call(compilation, "my test 1");
// 第二次调用
testhook.call(compilation, "my test 2");
// 第三次调用
testhook.call(compilation, "my test 3");
...
// 第n次调用
testhook.call(compilation, "my test n");

我们用这个demo做为用例,一步步debug。

SyncHook.js源码

主要逻辑如下:

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

// 继承 HookCodeFactory
class SyncHookCodeFactory extends HookCodeFactory {}

const factory = new SyncHookCodeFactory();

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

function SyncHook(args = [], name = undefined) {
    // 初始化 Hook
    const hook = new Hook(args, name);
    // 注意这里修改了 hook 的constructor
    hook.constructor = SyncHook;
    ...
    // 每个钩子都必须自行实现自己的 compile 方法!!!
    hook.compile = COMPILE;
    return hook;
}

Hook.js源码

主要逻辑如下:

// 问题一:思考一下为什么需要 CALL_DELEGATE
const CALL_DELEGATE = function(...args) {
    // 当第一次调用时,实际上执行的是 CALL_DELEGATE 方法
    this.call = this._createCall("sync");
    // 当第二次或者第n次调用时,此时 this.call 方法已经被设置成 this._createCall 的返回值
    return this.call(...args);
};
...
class Hook {
    constructor(args = [], name = undefined) {
        this._args = args;
        this.name = name;
        this.taps = []; // 存储我们通过 hook.tap 注册的插件
        this.interceptors = [];
        this._call = CALL_DELEGATE;
        // 初始化时,this.call被设置成CALL_DELEGATE
        this.call = CALL_DELEGATE;
        ...
        
        // 问题三:this._x = undefined 是什么
        this._x = undefined; // this._x实际上就是this.taps中每个插件的回调
        
        // 问题四:为什么需要在构造函数中绑定这些函数
        this.compile = this.compile;
        this.tap = this.tap;
        this.tapAsync = this.tapAsync;
        this.tapPromise = this.tapPromise;
    }
    // 每个钩子必须自行实现自己的 compile 方法。compile方法根据 this.taps以及 this._args动态生成函数执行体
    compile(options) {
        throw new Error("Abstract: should be overridden");
    }

    // 生成函数执行体
    _createCall(type) {
        return this.compile({
            taps: this.taps,
            interceptors: this.interceptors,
            args: this._args,
            type: type
        });
    }
    ...
    _tap(type, options, fn) {
        ...
        this._insert(options);
    }
    tap(options, fn) {
        this._tap("sync", options, fn);
    }
    _resetCompilation() {
        this.call = this._call;
        this.callAsync = this._callAsync;
        this.promise = this._promise;
    }
    _insert(item) {
        // 问题二:为什么每次调用 testhook.tap() 注册插件时,都需要重置this.call等方法?
        this._resetCompilation();
        ...
    }    
}

思考Hook.js源码中的几个问题

  • 问题一:为什么需要 CALL_DELEGATE
  • 问题二:为什么每次调用 testhook.tap() 注册插件时,都需要重置this.call等方法?
  • 问题三:this._x = undefined 是什么
  • 问题四:为什么需要在构造函数中绑定 this.compile this.tapthis.tapAsync 以及this.tapPromise等方法

当我们每次调用 testhook.tap 方法注册插件时,流程如下:

未命名文件.jpg

方法往this.taps数组中添加一个插件。this.__insert 方法逻辑比较简单,但这里有一个细节需要注意一下,为什么每次注册插件时,都需要调用this._resetCompilation()重置this.call等方法? 我们稍后再看下这个问题。先继续debug。

当我们 第一次(注意是第一次) 调用 testhook.call 时,实际上调用的是 CALL_DELEGATE 方法

const CALL_DELEGATE = function(...args) {
    // 当第一次调用时,实际上执行的是 CALL_DELEGATE 方法
    this.call = this._createCall("sync");
    // 当第二次或者第n次调用时,此时 this.call 方法已经被缓存成 this._createCall 的返回值
    return this.call(...args);
};

CALL_DELEGATE 调用 this._createCall 函数根据注册的 this.taps 动态生成函数执行体。并且 this.call 被设置成 this._createCall 的返回值缓存起来,如果 this.taps 改变了,则需要重新生成。

此时如果我们第二次调用 testhook.call 时,就不需要再重新动态生成一遍函数执行体。这也是tapable的优化技巧之一。这也回答了 问题一:为什么需要 CALL_DELEGATE

如果我们调用了n次 testhook.call,然后又调用 testhook.tap 注册插件,此时 this.call 已经不能重用了,需要再根据 CALL_DELEGATE 重新生成一次函数执行体,这也回答了问题二:为什么每次调用 testhook.tap() 注册插件时,都需要重置this.call等方法。可想而知重新生成的过程是很耗时的。因此我们在使用 tapable 时,最好一次性注册完所有插件,再调用 call

testhook.tap("plugin1");
testhook.tap("plugin2");
testhook.tap("plugin3");


testhook.call(compilation, "my test 1"); // 第一次调用 call 时,会调用CALL_DELEGATE动态生成函数执行体并缓存起来
testhook.call(compilation, "my test 2"); // 不会重新生成函数执行体,使用第一次的
testhook.call(compilation, "my test 3"); // 不会重新生成函数执行体,使用第一次的

避免下面的调用方式:

testhook.tap("plugin1");
testhook.call(compilation, "my test 1"); // 第一次调用 call 时,会调用CALL_DELEGATE动态生成函数执行体并缓存起来

testhook.tap("plugin2");
testhook.call(compilation, "my test 2"); // 重新调用CALL_DELEGATE生成函数执行体

testhook.tap("plugin3");
testhook.call(compilation, "my test 3"); // 重新调用CALL_DELEGATE生成函数执行体

现在让我们看看第三个问题,调用 this.compile 方法时,实际上会调用 HookCodeFacotry.js 中的 setup 方法:

setup(instance, options) {
    instance._x = options.taps.map(t => t.fn);
}

对于问题四,实际上这和 V8 引擎的 Hidden Class 有关,通过在构造函数中绑定这些方法,类中的属性形态固定,这样在查找这些方法时就能利用 V8 引擎中 Hidden Class 属性查找机制,提高性能。

HookCodeFactory.js

主要逻辑:

class HookCodeFactory {
    constructor(config) {
        this.config = config;
        this.options = undefined;
        this._args = undefined;
    }
    create(options){
    	this.init(options);
        let fn;
        switch (this.options.type) {
            case 'sync': 
                fn = new Function(
                    ...
                )
                break
            case 'async': 
                fn = new Function(
                    ...
                )
                break
            case 'promise': 
                fn = new Function(
                    ...
                )
                break
        }
        this.deinit();
        return fn;
    }
    setup(instance, options) {
        instance._x = options.taps.map(t => t.fn);
    }
    ...
}

手写 tapable 每个 Hook

手写 tapable中所有的 hook,并比较我们自己实现的 hook 和官方的执行时间

image.png

这里面每个文件都会实现一遍 官方的 hook,并比较执行时间,以 SyncHook 为例,批量注册1000个插件时,我们自己手写的 MySyncHook执行时间0.12ms,而官方的需要6ms,这中间整整50倍的差距!!!

image.png

具体可以看 我的仓库

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant
@lizuncong

Footer

© 2024 GitHub, Inc.

两个鬼故事乘胜追击百度云梦到被蛇咬是什么意思珠海起名柳传志语录大灌篮高清下载韩国经典三级属鼠的男孩子起名大全好听的博客名建筑物起名微软平板破事儿百度影音门店取名起名大全测试零食什么牌子起名验房程序保尔柯察金易名网站免费起名免费起名网有哪些网站二字公司起名大全给女猫咪起名字柳树的描写优美句子海底两万里读书笔记btv体育燕起名姓王有起名大全p2p理财公司排名徽商银行个人网上银行店起名美甲电信积分2020起名字公司女装品牌起名字好听少年生前被连续抽血16次?多部门介入两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”淀粉肠小王子日销售额涨超10倍高中生被打伤下体休学 邯郸通报单亲妈妈陷入热恋 14岁儿子报警何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言张家界的山上“长”满了韩国人?男孩8年未见母亲被告知被遗忘中国拥有亿元资产的家庭达13.3万户19岁小伙救下5人后溺亡 多方发声315晚会后胖东来又人满为患了张立群任西安交通大学校长“重生之我在北大当嫡校长”男子被猫抓伤后确诊“猫抓病”测试车高速逃费 小米:已补缴周杰伦一审败诉网易网友洛杉矶偶遇贾玲今日春分倪萍分享减重40斤方法七年后宇文玥被薅头发捞上岸许家印被限制高消费萧美琴窜访捷克 外交部回应联合利华开始重组专访95后高颜值猪保姆胖东来员工每周单休无小长假男子被流浪猫绊倒 投喂者赔24万小米汽车超级工厂正式揭幕黑马情侣提车了西双版纳热带植物园回应蜉蝣大爆发当地回应沈阳致3死车祸车主疑毒驾恒大被罚41.75亿到底怎么缴妈妈回应孩子在校撞护栏坠楼外国人感慨凌晨的中国很安全杨倩无缘巴黎奥运校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变王树国卸任西安交大校长 师生送别手机成瘾是影响睡眠质量重要因素国产伟哥去年销售近13亿阿根廷将发行1万与2万面值的纸币兔狲“狲大娘”因病死亡遭遇山火的松茸之乡“开封王婆”爆火:促成四五十对奥巴马现身唐宁街 黑色着装引猜测考生莫言也上北大硕士复试名单了德国打算提及普京时仅用姓名天水麻辣烫把捣辣椒大爷累坏了

两个鬼故事 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化