webpack-tapable-0.2.8 源码分析

栏目: JavaScript · 发布时间: 5年前

内容简介:webpack 是基于事件流的打包构建工具,也就是内置了很多 hooks。作为使用方,可以在这些钩子当中,去插入自己的处理逻辑,而这一切的实现都得益于 tapable 这个工具。它有多个版本,webpack 前期的版本是依赖于 tapable 0.2.8 这个版本,后来重构了,发了 2.0.0 beta 版本,因为源码都是通过字符串拼接,通过 new Function 的模式使用,所以看起来比较晦涩。那么既然如此,我们先从早期的 0.2.8 这个版本了解下它的前身,毕竟核心思想不会发生太大的变化。tapa

webpack 是基于事件流的打包构建工具,也就是内置了很多 hooks。作为使用方,可以在这些钩子当中,去插入自己的处理逻辑,而这一切的实现都得益于 tapable 这个工具。它有多个版本,webpack 前期的版本是依赖于 tapable 0.2.8 这个版本,后来重构了,发了 2.0.0 beta 版本,因为源码都是通过字符串拼接,通过 new Function 的模式使用,所以看起来比较晦涩。

那么既然如此,我们先从早期的 0.2.8 这个版本了解下它的前身,毕竟核心思想不会发生太大的变化。

tapable 的实现类似于 node 的 EventEmitter 的发布订阅模式。用一个对象的键存储对应的事件名称,键值来存储事件的处理函数,类似于下面:

function Tapable () {
  this._plugins = {
    'emit': [handler1, handler2, ......]
  }
}
复制代码

同时,原型上定义了不同的方法来调用 handlers。

我们先来看下用法,

  1. plugin与 applyPlugins

    void plugin(names: string|string[], handler: Function)
    void applyPlugins(name: string, args: any...)
    复制代码

    最基础的就是注册插件以及插件触发的回调函数。

    const Tapable = require('tapable')
    const t = new Tapable()
    
    // 注册插件
    t.plugin('emit', (...args) => {
      console.log(args)
      console.log('This is a emit handler')
    })
    
    // 调用插件
    t.applyPlugins('emit', '参数1')
    
    // 打印如下
    [ '参数1' ]
    This is a emit handler
    复制代码

    源码如下

    Tapable.prototype.applyPlugins = function applyPlugins(name) {
      if(!this._plugins[name]) return;
      var args = Array.prototype.slice.call(arguments, 1);
      var plugins = this._plugins[name];
      for(var i = 0; i < plugins.length; i++)
        plugins[i].apply(this, args);
    };
    
    Tapable.prototype.plugin = function plugin(name, fn) {
      if(Array.isArray(name)) {
        name.forEach(function(name) {
          this.plugin(name, fn);
        }, this);
        return;
      }
      if(!this._plugins[name]) this._plugins[name] = [fn];
      else this._plugins[name].push(fn);
    };
    复制代码

    很简单,内部维护 _plugins 属性来缓存 plugin 名称以及 handler。

  2. apply

    void apply(plugins: Plugin...)
    复制代码

    接收 plugin 作为参数,每个 plugin 必须提供 apply 方法,也就是 webpack 在编写 plugin 的规是插件实例必须提供 apply 方法。

    const Tapable = require('tapable')
    const t = new Tapable()
    
    // 声明一个 webpack 插件的类,对象必须声明 apply 方法
    class WebpackPlugin {
      constructor () {}
      apply () {
        console.log('This is webpackPlugin')
      }
    }
    
    const plugin = new WebpackPlugin()
    
    // tapable.apply
    t.apply(plugin) // print 'This is webpackPlugin'
    复制代码

    源码如下

    Tapable.prototype.apply = function apply() {
      for(var i = 0; i < arguments.length; i++) {
        arguments[i].apply(this);
      }
    };
    复制代码

    也很简单,依次执行每个插件的 apply 方法。

  3. applyPluginsWaterfall

    any applyPluginsWaterfall(name: string, init: any, args: any...)
    复制代码

    依次调用插件对应的 handler,传入的参数是上一个 handler 的返回值,以及调用 applyPluginsWaterfall 传入 args 参数组成的数组,说起来很绕,看看下面的例子:

    t.plugin('waterfall', (...args) => {
      // print ['init', 'args1']
      console.log(args)
      return 'result1'
    })
    
    t.plugin('waterfall', (...args) => {
      // print ['result1', 'args1']
      console.log(args)
      return 'result2'
    })
    
    const ret = t.applyPluginsWaterfall('waterfall', 'init', 'args1') // ret => 'result2'
    复制代码

    源码如下

    Tapable.prototype.applyPluginsWaterfall = function applyPluginsWaterfall(name, init) {
      if(!this._plugins[name]) return init;
      var args = Array.prototype.slice.call(arguments, 1);
      var plugins = this._plugins[name];
      var current = init;
      for(var i = 0; i < plugins.length; i++) {
        args[0] = current;
        current = plugins[i].apply(this, args);
      }
      return current;
    };
    复制代码

    上一个 handler 返回的值,会作为下一个 handler的第一个参数。

  4. applyPluginsBailResult

    any applyPluginsBailResult(name: string, args: any...)
    复制代码

    依次调用插件对应的 handler,传入的参数是 args,如果正执行的 handler 的 返回值不是 undefined,其余的 handler 都不会执行了。 bail 是保险的意思,即只要任意一个 handler 有 !== undefined 的返回值,那么函数的执行就终止了。

    t.plugin('bailResult', (...args) => {
      // [ '参数1', '参数2' ]
      console.log(args)
      return 'result1'
    })
    
    t.plugin('bailResult', (...args) => {
      // 因为上一个函数返回了 'result1',所以不会执行到这个handler
      console.log(args)
      return 'result2'
    })
    
    t.applyPluginsBailResult('bailResult', '参数1', '参数2')
    复制代码

    源码如下

    Tapable.prototype.applyPluginsBailResult = function applyPluginsBailResult(name, init) {
      if(!this._plugins[name]) return;
      var args = Array.prototype.slice.call(arguments, 1);
      var plugins = this._plugins[name];
      for(var i = 0; i < plugins.length; i++) {
        var result = plugins[i].apply(this, args);
        if(typeof result !== "undefined") {
          return result;
        }
      }
    };
    复制代码

    只要 handler 返回的值 !== undefined ,就会停止调用接下来的 handler。

  5. applyPluginsAsyncSeries & applyPluginsAsync(支持异步)

    void applyPluginsAsync(
      name: string,
      args: any...,
      callback: (err?: Error) -> void
    )
    复制代码

    applyPluginsAsyncSeries 与 applyPluginsAsync 的函数引用都是相同的,并且函数内部支持异步。callback 在所有 handler 都执行完了才会调用,但是在注册 handler 的时候,函数内部一定要执行 next() 的逻辑,这样才能执行到下一个 handler。

    t.plugin('asyncSeries', (...args) => {
      // handler 的最后一个参数一定是 next 函数
      const next = args.pop()
      // 执行 next,函数才会执行到下面的 handler
      setTimeout (() => {
        next()
      }, 3000)
    })
    
    t.plugin('asyncSeries', (...args) => {
      // handler 的最后一个参数一定是 next
      const callback = args.pop()
      // 执行 next,函数才会执行到 applyPluginsAsyncSeries 传入的 callback
      Promise.resolve(1).then(next)
    })
    
    t.applyPluginsAsyncSeries('asyncSeries', '参数1', (...args) => {
      console.log('这是 applyPluginsAsyncSeries 的 callback')
    })
    复制代码

    源码如下

    Tapable.prototype.applyPluginsAsyncSeries = Tapable.prototype.applyPluginsAsync = function applyPluginsAsyncSeries(name) {
      var args = Array.prototype.slice.call(arguments, 1);
      var callback = args.pop();
      var plugins = this._plugins[name];
      if(!plugins || plugins.length === 0) return callback();
      var i = 0;
      var _this = this;
      args.push(copyProperties(callback, function next(err) {
        if(err) return callback(err);
        i++;
        if(i >= plugins.length) {
          return callback();
        }
        plugins[i].apply(_this, args);
      }));
      plugins[0].apply(this, args);
    };
    复制代码

    applyPluginsAsyncSeries 内部维护了一个 next 函数,这个函数作为每个 handler 的最后一个参数传入,handler 内部支持异步操作,但是必须手动调用 next 函数,才能执行到下一个 handler。

  6. applyPluginsAsyncSeriesBailResult(支持异步)

    void applyPluginsAsyncSeriesBailResult(
      name: string,
      args: any...,
      callback: (result: any) -> void
    )
    复制代码

    函数支持异步,只要在 handler 里面调用 next 回调函数,并且传入任意参数,就会直接执行 callback。

    t.plugin('asyncSeriesBailResult', (...args) => {
      // handler 的最后一个参数一定是 next 函数
      const next = args.pop()
      // 因为传了字符串,导致直接执行 callback
      next('跳过 handler 函数')
    })
    
    t.plugin('asyncSeriesBailResult', (...args) => {
      
    })
    
    t.applyPluginsAsyncSeriesBailResult('asyncSeriesBailResult', '参数1', (...args) => {
      console.log('这是 applyPluginsAsyncSeriesBailResult 的 callback')
    })
    // print '这是 applyPluginsAsyncSeriesBailResult 的 callback'
    复制代码

    源码如下

    Tapable.prototype.applyPluginsAsyncSeriesBailResult = function applyPluginsAsyncSeriesBailResult(name) {
      var args = Array.prototype.slice.call(arguments, 1);
      var callback = args.pop();
      if(!this._plugins[name] || this._plugins[name].length === 0) return callback();
      var plugins = this._plugins[name];
      var i = 0;
      var _this = this;
      args.push(copyProperties(callback, function next() {
        if(arguments.length > 0) return callback.apply(null, arguments);
        i++;
        if(i >= plugins.length) {
          return callback();
        }
        plugins[i].apply(_this, args);
      }));
      plugins[0].apply(this, args);
    };
    复制代码

    applyPluginsAsyncSeriesBailResult 内部维护了一个 next 函数,这个函数作为每个 handler 的最后一个参数传入,handler 内部支持异步操作,但是必须手动调用 next 函数,才能执行到下一个 handler,next 函数可以传入参数,这样会直接执行 callback。

  7. applyPluginsAsyncWaterfall(支持异步)

    void applyPluginsAsyncWaterfall(
      name: string,
      init: any,
      callback: (err: Error, result: any) -> void
    )
    复制代码

    函数支持异步,handler 的接收两个参数,第一个参数是上一个 handler 通过 next 函数传过来的 value,第二个参数是 next 函数。next 函数接收两个参数,第一个是 error,如果 error 存在,就直接执行 callback。第二个 value 参数,是传给下一个 handler 的参数。

    t.plugin('asyncWaterfall', (value, next) => {
    // handler 的最后一个参数一定是 next 函数
      console.log(value)
      next(null, '来自第一个 handler')
    })
    
    t.plugin('asyncWaterfall', (value, next) => {
      console.log(value)
      next(null, '来自第二个 handler')
    })
    
    t.applyPluginsAsyncWaterfall('asyncWaterfall', '参数1', (err, value) => {
      if (!err) {
        console.log(value)
      }
    })
    
    // 打印如下
    
    参数1
    来自第一个 handler
    来自第二个 handler
    复制代码

    源码如下

    Tapable.prototype.applyPluginsAsyncWaterfall = function applyPluginsAsyncWaterfall(name, init, callback) {
      if(!this._plugins[name] || this._plugins[name].length === 0) return callback(null, init);
      var plugins = this._plugins[name];
      var i = 0;
      var _this = this;
      var next = copyProperties(callback, function(err, value) {
        if(err) return callback(err);
        i++;
        if(i >= plugins.length) {
          return callback(null, value);
        }
        plugins[i].call(_this, value, next);
      });
      plugins[0].call(this, init, next);
    };
    复制代码

    applyPluginsAsyncWaterfall 内部维护了一个 next 函数,这个函数作为每个 handler 的最后一个参数传入,handler 内部支持异步操作,但是必须手动调用 next 函数,才能执行到下一个 handler,next 函数可以传入参数,第一个参数为 err, 第二参数为上一个 handler 返回值。

  8. applyPluginsParallel(支持异步)

    void applyPluginsParallel(
      name: string,
      args: any...,
      callback: (err?: Error) -> void
    )
    复制代码

    并行的执行函数,每个 handler 的最后一个参数都是 next 函数,这个函数用来检验当前的 handler 是否已经执行完。

    t.plugin('parallel', (...args) => {
      const next = args.pop()
      console.log(1)
      // 必须调用 next 函数,要不然 applyPluginsParallel 的 callback 永远也不会回调
      next('抛出错误了1', '来自第一个 handler')
    })
    
    t.plugin('parallel', (...args) => {
      const next = args.pop()
      console.log(2)
      // 必须调用 next 函数,要不然 applyPluginsParallel 的 callback 永远也不会回调
      next('抛出错误了2')
    })
    
    t.applyPluginsParallel('parallel', '参数1', (err) => {
      // print '抛出错误了1'
      console.log(err)
    })
    复制代码

    源码如下

    Tapable.prototype.applyPluginsParallel = function applyPluginsParallel(name) {
      var args = Array.prototype.slice.call(arguments, 1);
      var callback = args.pop();
      if(!this._plugins[name] || this._plugins[name].length === 0) return callback();
      var plugins = this._plugins[name];
      var remaining = plugins.length;
      args.push(copyProperties(callback, function(err) {
        if(remaining < 0) return; // ignore
        if(err) {
          remaining = -1;
          return callback(err);
        }
        remaining--;
        if(remaining === 0) {
          return callback();
        }
      }));
      for(var i = 0; i < plugins.length; i++) {
        plugins[i].apply(this, args);
        if(remaining < 0) return;
      }
    };
    复制代码

    applyPluginsParallel 并行地调用 handler。内部通过闭包维护了 remaining 变量,用来判断内部的函数是否真正执行完,handler 的最后一个参数是一个函数 check。如果 handler 内部用户想要的逻辑执行完,必须调用 check 函数来告诉 tapable,进而才会执行 args 数组的最后一个 check 函数。

  9. ** applyPluginsParallelBailResult **(支持异步)

    void applyPluginsParallelBailResult(
      name: string,
      args: any...,
      callback: (err: Error, result: any) -> void
    )
    复制代码

    并行的执行函数,每个 handler 的最后一个参数都是 next 函数,next 函数必须调用,如果给 next 函数传参,会直接走到 callback 的逻辑。callback 执行的时机是跟 handler 注册的顺序有关,而不是跟 handler 内部调用 next 的时机有关。

    t.plugin('applyPluginsParallelBailResult', (next) => {
      console.log(1)
      setTimeout(() => {
        next('has args 1')
      }, 3000)
    })
    
    t.plugin('applyPluginsParallelBailResult', (next) => {
      console.log(2)
      setTimeout(() => {
        next('has args 2')
      })
    })
    
    t.plugin('applyPluginsParallelBailResult', (next) => {
      console.log(3)
      next('has args 3')
    })
    
    t.applyPluginsParallelBailResult('applyPluginsParallelBailResult', (result) => {
      console.log(result)
    })
    
    // 打印如下
    1
    2
    3
    has args 1
    
    虽然第一个 handler 的 next 函数是延迟 3s 才执行,但是注册的顺序是在最前面,所以 callback 的 result 参数值是 'has args 1'。
    复制代码

    源码如下

    Tapable.prototype.applyPluginsParallelBailResult = function applyPluginsParallelBailResult(name) {
      var args = Array.prototype.slice.call(arguments, 1);
      var callback = args[args.length - 1];
      if(!this._plugins[name] || this._plugins[name].length === 0) return callback();
      var plugins = this._plugins[name];
      var currentPos = plugins.length;
      var currentResult;
      var done = [];
      for(var i = 0; i < plugins.length; i++) {
        args[args.length - 1] = (function(i) {
          return copyProperties(callback, function() {
            if(i >= currentPos) return; // ignore
            done.push(i);
            if(arguments.length > 0) {
              currentPos = i + 1;
              done = fastFilter.call(done, function(item) {
                return item <= i;
              });
              currentResult = Array.prototype.slice.call(arguments);
            }
            if(done.length === currentPos) {
              callback.apply(null, currentResult);
              currentPos = 0;
            }
          });
        }(i));
        plugins[i].apply(this, args);
      }
    };
    复制代码

    for 循环里面并行的执行 handler,handler 的最后一个参数是一个匿名回调函数,这个匿名函数必须在每个 handler 里面手动的执行。而 callback 的执行时机就是根据 handler 的注册顺序有关。

从源码上来看,tapable 是提供了很多 API 来对应不同调用 handler 的场景,有同步执行,有异步执行,还有串行异步,并行异步等。这些都是一些高级的技巧,不管是 express,还是 VueRouter 的源码,都利用这些同异步执行机制,但是可以看出程序是有边界的。也就是约定成俗,从最后一个 applyPluginsParallel 函数来看,用户必须调用匿名回调函数,否则 tapable 怎么知道你内部是否有异步操作,并且异步操作在某个时候执行完了呢。

既然知道了 0.2.8 的核心思想,那么 2.0.0-beta 版的重构更是让人惊艳,目前的源码分析正在整理,链接如下。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

算法笔记

算法笔记

胡凡、曾磊 / 机械工业出版社 / 2016-7 / 65

这是一本零基础就能读懂的算法书籍,读者不需要因为自己没有语言基础而畏惧。书籍的第2章便是一个C语言的入门教程,内容非常易懂,并且十分实用,阅读完这章就可以对本书需要的C语言基础有一个较好的掌握。 本书已经覆盖了大部分基础经典算法,不仅可以作为考研机试和PAT的学习教材,对其他的一些算法考试(例如CCF的CSP考试)或者考研初试的数据结构科目的学习和理解也很有帮助,甚至仅仅想学习经典算法的读者......一起来看看 《算法笔记》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

SHA 加密
SHA 加密

SHA 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具