directory

    - src
        - sim    ---- 简单的模拟实现
        - /.js$/ ---- 使用

代码已上传github, 地址

Detailed

Webpack 就像一条生产线, 要经过一系列的处理流程才能将源文件转换成输出结果。这条生产线上的每个流程都是单一的, 多个流程之间存在依赖关系。只能完成当前处理后才会转交到下一个流程。

插件就像一个插入到生产线中的一个功能, 它会在特定的时机对生产线上的资源进行处理。

这条生产线很复杂, Webpack则是通过 tapable 核心库来组织这条生产线。

Webpack 在运行中会通过 tapable 提供的钩子进行广播事件, 插件只需要监听它关心的事件,就可以加入到这条生产线中,去改变生产线的运作。使得 Webpack整体扩展性很好。

Tapable Hook

Tapable 提供同步(Sync)和异步(Async)钩子类。而异步又分为 异步串行异步并行钩子类。

右键图片,在新标签中查看完整图片

逐个分析每个钩子类的使用及其原理

同步钩子类

  • SyncHook
  • SyncBailHook
  • SyncWaterfallHook
  • SyncLoopHook

同步钩子类通过实例的 tap 方法监听函数, 通过 call发布事件

SyncHook

同步串行不关心订阅函数执行后的返回值是什么。其原理是将监听(订阅)的函数存放到一个数组中, 发布时遍历数组中的监听函数并且将发布时的 arguments传递给监听函数

class SyncHook {
    constructor(options) {
        this.options = options
        this.hooks = []  //存放监听函数的数组
    }
    tap(name, callback) {
        this.hooks.push(callback)
    }
    call(...args) {
        for (let i = 0; i < this.hooks.length; i++) {
            this.hooks[i](...args)
        }
    }
}

const synchook = new SyncHook('name')
// 注册监听函数
synchook.tap('name', (data) => {
    console.log('name', data)
})
synchook.tap('age', (data) => {
    console.log('age', data)
})

// 发布事件
synchook.call('qiqingfu')

打印结果:

name qiqingfu
age qiqingfu

SyncBailHook

同步串行, 但是如果监听函数的返回值不为 null, 就终止后续的监听函数执行

class SyncBailHook {
        constructor(options) {
          this.options = options
          this.hooks = []
    }
    tap(name, callback) {
        this.hooks.push(callback)
    }
    call(...args) {
        let ret, i = 0
        do {
            // 将第一个函数的返回结果赋值给ret, 在while中如果结果为 true就继续执行do代码块
            ret = this.hooks[i++](...args)
        } while(!ret)
    }
}

const syncbailhook = new SyncBailHook('name')

syncbailhook.tap('name', (data) => {
    console.log('name', data)
    return '我的返回值不为null'
})
syncbailhook.tap('age', (data) => {
    console.log('age', data)
})

syncbailhook.call('qiqingfu')

执行结果

name qiqingfu

SyncWaterfallHook

同步串行瀑布流, 瀑布流指的是第一个监听函数的返回值,做为第二个监听函数的参数。第二个函数的返回值作为第三个监听函数的参数,依次类推...

class SyncWaterfallHook {
    constructor(options) {
          this.options = options
          this.hooks = []
    }
    tap(name, callback) {
        this.hooks.push(callback)
    }
    call(...args) {
        let [firstHook, ...otherHooks] = this.hooks
        /**
         * 通过解构赋值先取出第一个监听函数执行
         * 并且将第一个函数的执行结果传递给第二个, 第二个传递给第三个,迭代的过程
         */
        let ret = firstHook(...args)
        otherHooks.reduce((f,n) => {
            return n(f)
        }, ret)
    }
}

const syncWaterfallHook = new SyncWaterfallHook('name')

syncWaterfallHook.tap('name', data => {
    console.log('name', data)
    return 23
})
syncWaterfallHook.tap('age', data => {
    console.log('age', data)
})

syncWaterfallHook.call('qiqingfu')

打印结果

name qiqingfu
age 23

SyncLoopHook

同步串行, 如果监听函数的返回值为 true, 则反复执行当前的监听函数,直到返回指为 undefind则继续执行下面的监听函数

class SyncLoopHook {
  constructor(options) {
    this.options = options
    this.hooks = []
    }
    tap(name, callback) {
    this.hooks.push(callback)
    }
    call(...args) {
        for (let i = 0; i < this.hooks.length; i++) {
            let hook = this.hooks[i], ret
            do{
                ret = hook(...args)
            }while(ret === true && ret !== undefined)
        }
    }
}

const syncLoopHook = new SyncLoopHook('name')

let n1 = 0
syncLoopHook.tap('name', data => {
    console.log('name', data)
    return n1 < 2 ? true : undefined
})
syncLoopHook.tap('end', data => {
    console.log('end', data)
})

syncLoopHook.call('qiqingfu')

执行结果

name qiqingfu
name qiqingfu
name qiqingfu  第三次打印的时候, n1的指为2, 返回值为 undefined则执行后面的监听函数
end qiqingfu

异步钩子

  • 异步并行 (Parallel)

    • AsyncParallelHook
    • AsyncParalleBailHook
  • 异步串行 (Series)
    • AsyncSeriesHook
    • AsyncSeriesBailHook
    • AsyncSeriesWaterfallHook

凡有异步,必有回调

同步钩子是通过 tap来监听函数的, call来发布的。

异步钩子是通过 tapAsynctapPromise 来监听函数,通过 callAsyncpromise来发布订阅的。

AsyncParallelHook

异步并行, 监听的函数会一块执行, 哪个函数先执行完就先触发。不需要关心监听函数的返回值。

class AsyncParallelHook {
    constructor(options) {
        this.options = options
        this.asyncHooks = []
    }
    // 订阅
    tapAsync(name, callback) {
        this.asyncHooks.push(callback)
    }
    // 发布
    callAsync(...args) {
        /**
         * callAsync(arg1, arg2,..., cb)
         * 发布的时候最后一个参数可以是回调函数
         * 订阅的每一个函数的最后一个参数也是一个回调函数,所有的订阅函数执行完
         * 且都调用了最后一个函数,才会执行cb
         */
    const finalCallback = args.pop()
        let i = 0
        // 将这个作为最后一个参数传过去,使用的时候选择性调用
        const done = () => {
            ++i === this.asyncHooks.length && finalCallback()
        }
        this.asyncHooks.forEach(hook => {
            hook(...args, done)
        })
    }
}

const asyncParallelHook = new AsyncParallelHook('name')

asyncParallelHook.tapAsync('name', (data, done) => {
    setTimeout(() => {
    console.log('name', data)
    done()
  }, 2000)
})
asyncParallelHook.tapAsync('age', (data, done) => {
    setTimeout(() => {
    console.log('age', data)
    done()
  }, 3000)
})

console.time('time')
asyncParallelHook.callAsync('qiqingfu', () => {
  console.log('监听函数都调用了 done')
  console.timeEnd('time')
})

打印结果

name qiqingfu
age qiqingfu
监听函数都调用了 done
time: 3002.691ms

AsyncParalleBailHook

暂时不理解

AsyncSeriesHook

异步串行钩子类, 不关心 callback的参数。异步函数一个一个的执行,但是必须调用 done函数。

class AsyncSeriesHook {
    constructor(options) {
        this.options = options
        this.asyncHooks = []
    }
    tapAsync(name, callback) {
        this.asyncHooks.push(callback)
    }
    callAsync(...args) {
        const finalCallback = args.pop()

        let i = 0
        const done = () => {
            let task = this.asyncHooks[i++]
            task ? task(...args, done) : finalCallback()
        }
        done()
    }
}

const asyncSeriesHook = new AsyncSeriesHook('name')

asyncSeriesHook.tapAsync('name', (data, done) => {
    setTimeout(() => {
        console.log('name', data)
        done()
    }, 1000)
})

asyncSeriesHook.tapAsync('age', (data, done) => {
    setTimeout(() => {
        console.log('age', data)
        done()
    }, 2000)
})

console.time('time')
asyncSeriesHook.callAsync('qiqingfu', () => {
    console.log('end')
    console.timeEnd('time')
})

执行结果

name qiqingfu
age qiqingfu
end
time: 3010.915ms

AsyncSeriesBailHook

同步串行钩子类, callback的参数如果不是 null, 后面所有的异步函数都不会执行,直接执行 callAsync方法的回调函数

class AsyncSeriesBailHook {
    constructor(options) {
        this.options = options
        this.asyncHooks = []
    }
    tapAsync(name, callback) {
        this.asyncHooks.push(callback)
    }
    callAsync(...args) {
        const finalCallback = args.pop()

        let i = 0
        const done = data => {
      if (data) return finalCallback()
      let task = this.asyncHooks[i++]
      task ? task(...args, done) : finalCallback()
        }
        done()
    }
}

const asyncSeriesBailHook = new AsyncSeriesBailHook('name')

asyncSeriesBailHook.tapAsync('1', (data, done) => {
    setTimeout(() => {
        console.log('1', data)
        done(null)
    }, 1000)
})

asyncSeriesBailHook.tapAsync('2', (data, done) => {
    setTimeout(() => {
        console.log('2', data)
        done(null)
    }, 2000)
})

console.time('times')
asyncSeriesBailHook.callAsync('qiqingfu', () => {
    console.log('end')
    console.timeEnd('times')
})

打印结果

1 qiqingfu
2 qiqingfu
end
times: 3012.060ms

AsyncSeriesWaterfallHook

同步串行钩子类, 上一个监听函数 callback(err, data)的第二个参数, 可以作为下一个监听函数的参数

class AsyncSeriesWaterfallHook {
    constructor(options) {
        this.options = options
        this.asyncHooks = []
    }
    tapAsync(name, callback) {
        this.asyncHooks.push(callback)
    }
    callAsync(...args) {
        const finalCallback = args.pop()

        let i = 0, once
        const done = (err, data) => {
            let task = this.asyncHooks[i++]
            if (!task) return finalCallback()
            if (!once) {
                // 只执行一次
                task(...args, done)
                once = true
            } else {
                task(data, done)
            }
        }
        done()
    }
}

const asyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook('name')

asyncSeriesWaterfallHook.tapAsync('1', (data, done) => {
    setTimeout(() => {
        console.log('1', data)
        done(null, '第一个callback传递的参数')
    }, 1000)
})

asyncSeriesWaterfallHook.tapAsync('2', (data, done) => {
    setTimeout(() => {
        console.log('2', data)
        done(null)
    }, 1000)
})

console.time('timer')
asyncSeriesWaterfallHook.callAsync('qiqingfu', () => {
    console.log('end')
    console.timeEnd('timer')
})

打印结果

1 qiqingfu
2 第一个callback传递的参数
end
timer: 2015.445ms

END

如果理解有误, 麻烦纠正!

参考文章

webpack4.0源码分析之Tapable

webpack 4.0 Tapable 类中的常用钩子函数源码分析

Webpack Tapable原理详解的更多相关文章

  1. I2C 基础原理详解

    今天来学习下I2C通信~ I2C(Inter-Intergrated Circuit)指的是 IC(Intergrated Circuit)之间的(Inter) 通信方式.如上图所以有很多的周边设备都 ...

  2. Zigbee组网原理详解

    Zigbee组网原理详解 来源:互联网 作者:佚名2015年08月13日 15:57   [导读] 组建一个完整的zigbee网状网络包括两个步骤:网络初始化.节点加入网络.其中节点加入网络又包括两个 ...

  3. 块级格式化上下文(block formatting context)、浮动和绝对定位的工作原理详解

    CSS的可视化格式模型中具有一个非常重要地位的概念——定位方案.定位方案用以控制元素的布局,在CSS2.1中,有三种定位方案——普通流.浮动和绝对定位: 普通流:元素按照先后位置自上而下布局,inli ...

  4. SSL/TLS 原理详解

    本文大部分整理自网络,相关文章请见文后参考. SSL/TLS作为一种互联网安全加密技术,原理较为复杂,枯燥而无味,我也是试图理解之后重新整理,尽量做到层次清晰.正文开始. 1. SSL/TLS概览 1 ...

  5. 锁之“轻量级锁”原理详解(Lightweight Locking)

    大家知道,Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意. 原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖 ...

  6. [转]js中几种实用的跨域方法原理详解

    转自:js中几种实用的跨域方法原理详解 - 无双 - 博客园 // // 这里说的js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同 ...

  7. 节点地址的函数list_entry()原理详解

    本节中,我们继续讲解,在linux2.4内核下,如果通过一些列函数从路径名找到目标节点. 3.3.1)接下来查看chached_lookup()的代码(namei.c) [path_walk()> ...

  8. WebActivator的实现原理详解

    WebActivator的实现原理详解 文章内容 上篇文章,我们分析如何动态注册HttpModule的实现,本篇我们来分析一下通过上篇代码原理实现的WebActivator类库,WebActivato ...

  9. Influxdb原理详解

    本文属于<InfluxDB系列教程>文章系列,该系列共包括以下 15 部分: InfluxDB学习之InfluxDB的安装和简介 InfluxDB学习之InfluxDB的基本概念 Infl ...

随机推荐

  1. Android - Rxjava 使用和原理

    用RxJava写的一个Android的小Demo 我所理解的RxJava——上手其实很简单 http://www.jianshu.com/p/5e93c9101dc5

  2. HashMap和Hashtable的比较

    相同点 HashMap和Hashtable都是存储“键值对(key-value)”的散列表,而且都是采用拉链法解决hash冲突的.但是1.8中,hashmap引入了红黑树.Hashtable没有引入红 ...

  3. JS多级树结构写法

    效果: 一.布局: <div class="three_tree"> <div class="tree_title_cut"> < ...

  4. Zepto结合Swiper的选项卡

    我们昨天说了关于Angular的选项卡,那今天就说一下Swiper的选项卡吧! 今天的选项卡是Zepto结合Swiper的选项卡,咱么明天再说纯纯的Swiper的吧! 既然是关于Zepto和Swipe ...

  5. Oracle数据库错误消息

    Oracle数据库错误消息 导出错误消息 l EXP-00000导出终止失败 原因:导出时产生Oracle错误. 操作:检查相应的Oracle错误消息. l EXP-00001数据域被截断 - 列长度 ...

  6. 使用CKRule实现促销管理系统

    1, 常见的促销模型 促销管理系统在很多地方都有使用,大家去超市就经常体现到,感受到,不少中小型单位都其促销活动,要搞促销活动最好是有应用软件支持,这样就比较灵活管理也方便.而依靠手工处理的话效率会比 ...

  7. 【起航计划 033】2015 起航计划 Android APIDemo的魔鬼步伐 32 App->Service->Foreground Service Controller service使用,共享service,前台服务,onStartCommand

    Android系统也提供了一种称为“Service”的组件通常在后台运行.Activity 可以用来启动一个Service,Service启动后可以保持在后台一直运行,即使启动它的Activity退出 ...

  8. java中什么是上下文(servletContext)

    找了很多大佬的博客,看了之后还不是很清楚上下文到底是怎么回事,我个人理解 所谓上下文,它是用来存储系统的一些初始化信息,例如在jboss中通过配置文件指定了数据源,那么在jboss启动的时候就把这个文 ...

  9. Siebel escript学习笔记

    Siebel(escript)的学习:1.Siebel的数据类型Primitive(原始的)---Number,Integer,Hexadecimal(十六进制),Octal(八进制),Floatin ...

  10. springMvc-入参对象

    1.修改或者添加对象 2.多添件查询时候也会遇到 springMvc能够根据属性自动的封装pojo的对象并且支持关联的对象:大致的原理是在传入后台的时候把前台的属性和对象封装成json的形式传入后台, ...