webpack-tapable-0.2.8 源碼分析

webpack 是基於事件流的打包構建工具,也就是內置了不少 hooks。做爲使用方,能夠在這些鉤子當中,去插入本身的處理邏輯,而這一切的實現都得益於 tapable 這個工具。它有多個版本,webpack 前期的版本是依賴於 tapable 0.2.8 這個版本,後來重構了,發了 2.0.0 beta 版本,由於源碼都是經過字符串拼接,經過 new Function 的模式使用,因此看起來比較晦澀。html

那麼既然如此,咱們先從早期的 0.2.8 這個版本瞭解下它的前身,畢竟核心思想不會發生太大的變化。node

tapable 的實現相似於 node 的 EventEmitter 的發佈訂閱模式。用一個對象的鍵存儲對應的事件名稱,鍵值來存儲事件的處理函數,相似於下面:webpack

function Tapable () {
  this._plugins = {
    'emit': [handler1, handler2, ......]
  }
}
複製代碼

同時,原型上定義了不一樣的方法來調用 handlers。git

咱們先來看下用法,github

  1. pluginapplyPluginsweb

    void plugin(names: string|string[], handler: Function)
    void applyPlugins(name: string, args: any...)
    複製代碼

    最基礎的就是註冊插件以及插件觸發的回調函數。express

    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 版的重構更是讓人驚豔,目前的源碼分析正在整理,連接以下

相關文章
相關標籤/搜索