zoukankan      html  css  js  c++  java
  • webpack 源码从零开始:apable模型

    前文

    最近在看webpack的源码,发现有个比较头疼的点是:代码看起来非常跳跃,往往看不到几行就插入一段新内容,为了理解又不得不先学习相关的前置知识。层层嵌套之后,发现最基础的还是tapable模型,因此先对这部分的内容做一个介绍。

    引子-webpack的基本流程

    Webpack的流程可以分为以下三大阶段:

    初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。这个compile对象会穿行在本次编译的整个周期。
    编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
    输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

    在这个过程中,最核心的就是插件化的设计: 在不同的阶段执行相应的一些插件,来执行某些功能。
    而这里的阶段,指的就是hook。 理论太抽象,来看一段webpack的源码(4.x版本):

    // webpack/lib/MultiCompiler.js
    const { Tapable, SyncHook, MultiHook } = require("tapable");
    class MultiCompiler extends Tapable {
        constructor(compilers) {
            super();
            this.hooks = {
                done: new SyncHook(["stats"]),
                invalid: new MultiHook(compilers.map(c => c.hooks.invalid)),
                run: new MultiHook(compilers.map(c => c.hooks.run)),
                watchClose: new SyncHook([]),
                watchRun: new MultiHook(compilers.map(c => c.hooks.watchRun)),
                infrastructureLog: new MultiHook(
                    compilers.map(c => c.hooks.infrastructureLog)
                )
            };
        }
        /// 省略其他代码
    }

    这是compile的构造函数,有几个注意点:

    1. 显示继承了Tapable,也就是本文的话题对象
    2. 注意看this.hooks部分的内容: done invalid, run, watchClose 等等都是内置的生命周期,具体的代码暂时不去关心。

    这部分代码主要是为了说明一个思路: webpack 的生命周期hook,实际上是一个个插件的集合,代表的含义是,在某个阶段需要挂载某些插件。 
    到这里,脑海里有这种大概雏形就好,接下来我们开始介绍Tapable。

    Tapable机制初探

    Tapable的核心思路有点类似于nodejs中的events,最基本的发布/订阅模式。

    const EventEmitter = require('events');
    const myEmitter = new EventEmitter();
    
    // 注册事件对应的监听函数
    myEmitter.on('安歌发布新文章', (title, tag) => {
        console.log("前去围观并吐槽",title, tag)
    });
    
    // 触发事件 并传入参数
    myEmitter.emit('安歌发布新文章',’标题tapable机制‘, '标签webpack');

    这个结构很简单也很清晰:

    1. events.on 用于注册要监听的事件和对应的毁掉方法
    2. events.emit 用于触发对应的事件

    tapable的核心用法与此相似,那为什么多次一举要使用它呢?

    根据前面的demo,不妨假设一下,如果我们注册了很多事件,比如event.on(’起床‘),event.on(’吃饭‘),event.on(’上班‘)等等,那事件之间可能就存在一些依赖关系,比如要先起床然后才能上班这样的时序依赖,而tapable就可以帮助我们很方便的管理这些关系。

     

    基本用法

    接下来用一个前几天参加的公司中秋晚会的例子,来简单说明一下Tapable的用法:
    我把自己的参加流程分成以下阶段:

    1. 晚宴前

      1. 检查着装
      2. 乘坐班车到酒店
    2. 晚宴中

      1. 用餐并欣赏表演
      2. 在当前桌进行博饼
      3. 如果成为当前桌状元,那么就留下来博王中王,
    3. 晚宴后

      1. 拍一些照片,打车回家
      2. 用拍好的照片发朋友圈纪念

    那么先写个全局demo:

    // 1. 引入 tapable ,先不管具体的钩子类型
    const {
        SyncHook,
        SyncBailHook,
        SyncWaterfallHook,
     } = require("tapable");
    
    // 2. 定义不同阶段对应的钩子,
    
    // 钩子: 晚宴前
    let beforeDinner = new SyncHook(["stageName"]);
    // 钩子:晚宴中
    let atTheDinner = new SyncBailHook(["stageName"]);
    // 钩子 晚宴后
    let afterDinner = new SyncWaterfallHook(["stageName"]);
    
    
    // 3. 为不同阶段注册事件,这里先写出晚宴前的事件
    
    beforeDinner.tap('检查着装', (stageName)=>{
        console.log(`${stageName}: 检查着装`)
    })
    
    beforeDinner.tap('乘坐班车到酒店', (stageName)=>{
        console.log(`${stageName}: 乘坐班车到酒店`)
    })
    
    
    // 每个阶段触发自身需要执行的事件
    beforeDinner.call('晚宴前');
    atTheDinner.call('晚宴中');
    afterDinner.call('晚宴后');
    
    // 输出结果:
    // 晚宴前: 检查着装
    // 晚宴前: 乘坐班车到酒店
    // ... 省略后面的输出

    这个demo简单的定义了三个阶段,先不去关具体的hook类型,了解下整体的结构:

    1. beforeDinner在实例化时,使用数组声明了参数stageName, 这个地方的参数类型仅仅作为接口定义的目的使用,为了方便触发的时候传入对应的参数;
    2. call方法其实就类似前文的emit,与之不同的是,event.emit表示事件触发,而hook.call表示当前钩子要开始执行钩子上注册的所有事件。(当然我们只注册了晚宴前的2个事件)
    3. hook.call(param)执行之后,该hook对应的事件就按照注册顺序以及特定规则(具体规则后面说明,暂时略过)依次执行,因此上面的beforeDinner.call('晚宴前');会输出对应的阶段名称和事件名称。

    到这里,我们已经用上了最基本的tapable了。回顾下它和events最大的区别:

    tapable不仅提供了事件的注册和执行,还用不同的Hook 将事件进行分类(这里例子用三个阶段将基础事件分类)

    SyncBailHook

    接下来就是晚宴中的事件,这里有个注意点:晚宴中的第三个事件”如果成为当前桌状元,那么就留下来博王中王“是一个带有前提条件的事件,所以我们用了SyncBailHook,并且这么注册事件:

    atTheDinner.tap('用餐并欣赏表演', (stageName) => {
        console.log(`${stageName}: 用餐并欣赏表演`);
    })
    
    atTheDinner.tap('在当前桌进行博饼', (stageName) => {
        console.log(`${stageName}: 在当前桌进行博饼`); 
        // 关键伪代码
        let getChampion = false //如果获得状元
        if(!getChampion){
            console.log(`${stageName}: 没有获得当前桌状元,不需要参与博王中王`);  // 注意这里的return
            return '提前结束!';
        }
    })
    
    atTheDinner.tap('博王中王', (stageName) => {
        console.log(`${stageName}: 博王中王`);
    })
    

    SyncBailHook翻译过来意思是“熔断类型的钩子”,作用就像保险丝,一旦有危险,则启动保护(一旦该钩子的某个事件,执行返回除了undefined以外的值,后面注册的事件就不再执行)。正如前面的例子中,如果在“当前桌子博饼”中没有成功搏到“状元”,就不会进行后面的“搏王中王”事件。常用于处理某些需要条件判断才触发的事件。

    SyncWaterfallHook

    晚宴之后的事件,与前面不用的地方在于:事件2发朋友圈 用的是事件1中所拍的照片,换句话说后面的事件依赖于前面事件的执行结果。所以可以这么写:

    afterDinner.tap('回家前拍照',  (stageName) => {
        console.log(`${stageName}: 拍一些照片,打车回家`);
        let pictures = ['image1','image2'];
        return pictures; 
    })
    
    afterDinner.tap('回家后发朋友圈', (pictures)=> {
       // 注意这里的内置参数 不再是stageName 而是pictures
       return console.log(`回家后,用${pictures}:发朋友圈`);
    })

    实例化afterDinner时使用了SyncWaterfallHook,顾名思义,这种瀑布式的钩子,作用就是在执行该钩子内注册的事件时,会把每个阶段的执行结果传递给后面的阶段。

    小结

    这部分我们介绍了tapable的基本用法和三种基本类型的hook,大概可以总结一下:

    hook表示事件的集合,hook的类型决定了注册在这个hook的事件如何执行

    异步类型的hook

    总览

    开胃菜结束,接下来要真正开始系统化的了解tapable了,(好消息是如果前面的例子都看懂了,后面的学起来会非常简单,坏消息是:又要涉及前端最棘手的问题之一--异步)

    先来一览所有的hook类型:

    总体上,hook类型分成同步和异步两大类,异步再分为异步串行和异步并行。

    先前已经介绍了同步hook里面的前三种。第四种SynloopHook也简要介绍下:

    假设写文章这个事情,分成校对发表两个步骤,校对必须3次以上,才可以执行发表事件:

    // 当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环
    let writeArticle  = SyncLoopHook();
    let count = 0;
    
    writeArticle.tap('校对',()=>{
        console.log('执行校对', count++)
        if(count<3){
            return true; // 没有达到3次则继续校对
        }
        return
    })
    
    writeArticle.tap('发表',()=>{
        console.log('发表')
    })

    异步并行 AsyncParallelHook

    异步的hook,注册和触发可以用tapAsync/callAsync和tapPromise/promise两种语法,写法上略有不用。直接上demo:

    // AsyncParallelHook 钩子:tapAsync/callAsync 的使用
    const { AsyncParallelHook } = require("tapable");
    
    // 创建实例
    let asyncParallelHook = new AsyncParallelHook(["demoName"]);
    
    // 注册事件
    console.time("time");
    asyncParallelHook.tapAsync("异步事件1", (demoName, done) => {
        setTimeout(() => {
            console.log("1", demoName,  new Date());
            done(); //需要注意的是这里的`done`方法 
        }, 1000);
    });
    
    asyncParallelHook.tapAsync("异步事件2", (demoName, done) => {
        setTimeout(() => {
            console.log("2", demoName,  new Date());
            done();
        }, 2000);
    });
    
    asyncParallelHook.tapAsync("异步事件3", (demoName, done) => {
        setTimeout(() => {
            console.log("3", demoName,  new Date());
            done();
            console.timeEnd("time");
        }, 3000);
    });
    
    // 触发事件,让监听函数执行
    asyncParallelHook.callAsync("异步并行", () => {
        // 只有当前钩子的所有事件都执行done 才进入这个callback
        console.log("complete");
    });
    
    // 输出
    // 异步事件1 异步并行
    // Sun Sep 08 2019 21:24:12 GMT+0800 (GMT+08:00) {}
    // 异步事件2 异步并行
    // Sun Sep 08 2019 21:24:13 GMT+0800 (GMT+08:00) {}
    // 异步事件3 异步并行
    // Sun Sep 08 2019 21:24:14 GMT+0800 (GMT+08:00) {}
    // complete
    // time: 3007.266845703125ms
    // time: 3007.640ms 

    需要注意的是这里的done方法, 每个注册的的事件都可以调用到这个done方法,这个方法的作用是:向对应的hook实例告知,当前的异步事件完成,只有当所有的事件回调都执行了done方法,才会进入钩子本身的回调函数(demo中的console.log("complete");)

    从例子中的计时情况来看,很明显所有的事件是并行的 -- 事件1 2 3分别需要1s 2s 3s, 最终执行完也只花了3s。

    用tapPromise/promise来写的话,如下:

    asyncParallelHook.tapPromise("异步事件1", (demoName) => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log("1", demoName,  new Date());
                resolve("1");
            }, 1000);
        });
    });
    
    // ...省略重复代码
    asyncParallelHook.promise("异步并行").then(() => {
        console.log("最终结果", new Date());
    }).catch(err => {
        console.log("发现错误", new Date());
    });

    区别在于:

    1. 使用tapPromise注册时,回调函数必须返回一个promise
    2. 使用tabAsync注册使用done表示当前执行完成,使用tapPromise时则只要使用resolve()即可
    3. 如果其中一个事件没有resolve, 而是reject(error),那么会进入asyncParallelHook的catch而不是then

    这种写法其实很类似ES6中的promise.all,比较好理解

    异步串行AsyncSeriesHook

    其实到这里,已经一只脚踏进成功的大门了。 异步串行和异步并行的写法,完全一样。只需要简单把前面例子中,实例化的语句改成:

    let asyncSeriesHook = new AsyncSeriesHook()

    然后看看3个异步事件执行完后的事件间隔(并行的时候是3s,串行时总时长变成6s)。

    没错,就是这么简单~!

    广州VI设计公司https://www.houdianzi.com

    引申 -- 实例案例webpack-dev-middleware中tapable的应用

    webpack-dev-middleware是一个webpack的插件,作用是监听webpack的编译变化并写入到内存中。 核心代码:

    // webpack-dev-middleware/lib/context.js
    
    const context = {
        state: false, 
        webpackStats: null, // 
        callbacks: [],
        options,
        compiler,
        watching: null,
        forceRebuild: false,
      };
      
       function invalid(callback) {
        if (context.state) {
          context.options.reporter(context.options, {
            log,
            state: false,
          });
        }
    
        // We are now in invalid state
        context.state = false;
        if (typeof callback === 'function') {
          callback();
        }
      }
    
      
      // 关键代码 利用compile的hook 观察编译变化 并插入操作
      context.compiler.hooks.invalid.tap('WebpackDevMiddleware', invalid);
      context.compiler.hooks.run.tap('WebpackDevMiddleware', invalid);
      context.compiler.hooks.done.tap('WebpackDevMiddleware', done);
      context.compiler.hooks.watchRun.tap(
        'WebpackDevMiddleware',
        (comp, callback) => {
          invalid(callback);
        }
      );

    核心的代码就是使用webpack提供的内置hook watchRun来插入自定义的操作(检查编译情况,生成临时结果到内存)

    总结

    呼~ tapable的内容大概写完了,本文介绍了同步的几种钩子,和异步的2种代表性的钩子,至于异步并行熔断等等,就是前面介绍的钩子的合成,比较简单。回顾一下主要的内容:

    1. 同步hook触发后,按照事件注册顺序依次调用,并根据钩子类型,有一些特殊行为(bail loop);
    2. 异步的hook有tapAsync/callAsync和tapPromise/promise两种使用方式,并且都可以观察事件整体执行结果;
    3. 异步串行和异步并行的区别,在于注册的事件依次执行(前一个完成才开始执行后一个)还是并发执行;

    理解清楚tapable之后,再开始学习webpack的源码,会相对顺畅一些。

  • 相关阅读:
    网站宣传【免费】
    gridview行链接
    提前预告
    C#中var、int、object性能比较
    20部软件测试视频教程整合
    PO、VO、DTO、POJO
    云计算、虚拟化、容器
    Linux打包和压缩的区别
    Linux之Shell定时备份数据库
    luogu P1997 faebdc的烦恼 | 莫队
  • 原文地址:https://www.cnblogs.com/qianxiaox/p/14025842.html
Copyright © 2011-2022 走看看