Webpack Hook杂谈

544 阅读2分钟

Tapable

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");

Tapable提供了上述9中hook。详细的api方法可以查看Tapable文档

Tapable主要由两个重要部分组成

  1. Hook
  2. HookCodeFactory

下面以SyncHook为例,我们看看Hook处理的整个流程。SyncHook是Tapable中最容易理解的Hook,因此作为Demo进行分析。

Demo代码如下:

class Car {
  constructor() {
    this.hooks = {
       accelerate: new SyncHook(["newSpeed"]),
    };
  }
  
  setSpeed(newSpeed) {
    this.hooks.accelerate.call(newSpeed);
  }
}

const myCar = new Car();

myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed1 => {
  console.log(`${newSpeed1}`)
});

myCar.hooks.accelerate.tap("LoggerPlugin2", newSpeed2 => {
  console.log(`${newSpeed2}`)
})

myCar.hooks.accelerate.tap("LoggerPlugin3", newSpeed3 => {
  console.log(`${newSpeed3}`)
})

myCar.setSpeed(100);

注册插件

myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed1 => {
  console.log(`${newSpeed1}`)
});

首先我们一起看看tap方法(代码经过部分删减和转换)。

tap(options, fn) {
  if (typeof options === "string") options = { name: options };
  options = Object.assign({ type: "sync", fn: fn }, options);
  this.taps.push(item);
}

tap方法主要是把输入的两个参数(plugin的名称plugin的主要逻辑)组成一个带有type的对象,然后存放到taps数组中。

taps数组中存放的对象如下所示。

{
    name: "LoggerPlugin1",
    type: "sync",
    fn: (newSpeed1) => {
        console.log(`${newSpeed1}`)
    }
}

Hook的触发

接下来我们一起看看hook的call方法做了些什么。

this.hooks.accelerate.call(newSpeed)

其实call是一个闭包。完成了把注册好的plugin按照一定的规则执行。而这个执行的规则则是由_createCall创建。_createCall会调用compile方法,compile是由Hook的子类进行实现(这里就是由SyncHook来实现)。

class Hook {
  constructor(args) {
    if (!Array.isArray(args)) args = [];
      this._args = args;
      this.taps = [];
      this.call = this._call;
      this._x = undefined;
  }

  _createCall(type) {
    return this.compile({
      taps: this.taps,
      args: this._args,
      type: type
    });
  }
	
  tap(options, fn) {...}
  ...
}

function createCompileDelegate(name, type) {
  return function lazyCompileHook(...args) {
    this[name] = this._createCall(type);
    return this[name](...args);
  };
}

Object.defineProperties(Hook.prototype, {
  _call: {
    value: createCompileDelegate("call", "sync"),
    configurable: true,
    writable: true
  },
})

每个Hook都有一个对应的HookCodeFactoryHookCodeFactory的作用就是创建一个根据规则创建待执行plugin的函数。HookCodeFactory里面大部分代码是都是在拼接函数。

const factory = new SyncHookCodeFactory();

class SyncHook extends Hook {
  compile(options) {
    factory.setup(this, options);
    return factory.create(options);
  }
}

以下我将简化SyncHookCodeFactory代码,代码和源代码并不一致,只是为了说明code是怎样生成的。

HookCodeFactory,是用动态Function构建Hook触发的Plugin执行方法。

为什么要用new Function?

因为create的过程是动态的,不可能预先写好方法,因此用动态的Function也是一种解决方案。

class SyncHookCodeFactory {
  constructor() {
    this.options = undefined;
    this._args = [];
  }

  create(options) {
    this.init(options);
    const fn = new Function(
      this.args(),
      this.content()
    );
    return fn;
  }

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

  init(options) {
    this.options = options;
    this._args = options.args.slice();
  }

  content() {
    let code = '"use strict";\nvar _x = this._x;\n';
    if (this.options.taps.length === 0) { return code; }
    for (let j = this.options.taps.length - 1; j >= 0; j--) {
      code += `var _fn${j} = ${this.getTapFn(j)};\n`;
      code += `_fn${j}(${this.args()});\n`;
    }
    return code;
  }

  args() {
    return this._args.join(', ');
  }

  getTapFn(idx) {
    return `_x[${idx}]`;
  }
}

在本例子中(串行钩子),执行factory的create方法后,会返回一个函数,参数即为call方法传入的参数:

function anonymous(newSpeed) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(newSpeed);
    var _fn1 = _x[1];
    _fn1(newSpeed);
    var _fn2 = _x[2];
    _fn2(newSpeed);
}

需要说一下,这里的_x,其实由taps.map(t => t.fn)得到的。简单来说就是注册的plugin列表。 下面简单地把_x数组所代表的内容列出来。

// 以_x[0]为例子
_x[0] = newSpeed1 => {
  console.log(`${newSpeed1}`)
}

AsyncParallelHook与AsyncSeriesHook

因为在一篇博文中看到, AsyncParallelHookAsyncSeriesHook两个执行异步的方法(文中是settimeout),执行时间是不一致的。AsyncParallelHook和它名字一样,是并行执行的;相反AsyncSeriesHook是串行执行的。

由于名字都是带async的,给人的错觉是都是异步并行。于是做了Demo验证一下。

class Car {
  constructor() {
    this.hooks = {
      // 这里是AsyncParallelHook与AsyncSeriesHook切换
      // calculateRoutes: new AsyncParallelHook(["name"])
      calculateRoutes: new AsyncSeriesHook(["name"])
    };
  }
  useNavigationSystemAsync(name) {
    this.hooks.calculateRoutes.callAsync(name, err => {
      console.log(err);
    });
  }
}

const myCar = new Car();

myCar.hooks.calculateRoutes.tapAsync("TapAsync1", (name, cb) => {
  console.log(name, 1);
  cb();
});

myCar.hooks.calculateRoutes.tapAsync("TapAsync2", (name, cb) => {
  console.log(name, 2);
  cb();
});

myCar.useNavigationSystemAsync('webpack')

AsyncSeriesHookFactory产生的代码如下

function anonymous(name, _callback) {
    "use strict";
    function _next0() {
      const _fn1 = _x[1];
      _fn1(name, _err1 => {
        if (_err1) {
          _callback(_err1);
        } else {
          _callback();
        }
      });
    }
    const _fn0 = _x[0];
    _fn0(name, _err0 => {
      if (_err0) {
        _callback(_err0);
      } else {
        _next0();
      }
    });
}

AsyncParallelHookFactory产生的代码如下

function anonymous(name, _callback) {
    "use strict";
    do {
      var _counter = 2;
      var _done = () => {
        _callback();
      };
      if (_counter <= 0) { break; }
      const _fn0 = _x[0];
      _fn0(name, _err0 => {
        if (_err0) {
          if (_counter > 0) {
            _callback(_err0);
            _counter = 0;
          }
        } else if (--_counter === 0) { _done(); }
      });
      if (_counter <= 0) { break; }
      const _fn1 = _x[1];
      _fn1(name, _err1 => {
        if (_err1) {
          if (_counter > 0) {
            _callback(_err1);
            _counter = 0;
          }
        } else if (--_counter === 0) { _done(); }
      });
      if (_counter <= 0) { break; }
    } while (false);
}

首先,我们可以得知如果在callback中传入参数,后续的插件都都不会执行。

  • AsyncSeriesHookFactory 可以看到,执行完_fn0(即第一个插件)后,才会调用_next0()执行_fn1
  • AsyncParallelHookFactory 则不同,所有的函数几乎是同时执行,每个回调执行完count减一,直到count为0执行done方法(done方法就是下面的这个)
err => {
    console.log(err);
}

区别于EventEmitter

Tapable的写法与传统的事件驱动机制不太一样,但它做的事情都是差不多。都是需要有一个订阅“事件”方法,和触发“事件”方法。
虽然说机制比较相似,但提供了9种基本的触发策略的Tapable可以说更加强大。

相似处

先说说它们之间相似的地方,以SyncHook为例来对比的话,SyncHook基本可以用EventEmitter实现。

Tapable的tap作用相当于EventEmitter的on;而call作用就相当于emit

// SyncHook
const accelerateHook = new SyncHook(["newSpeed"])

accelerateHook.tap("LoggerPlugin", newSpeed => {
  console.log(`${newSpeed}`)
});

accelerateHook.call(100);

// Node EventEmitter
const eventEmitter = new EventEmitter();

eventEmitter.on("accelerate", newSpeed1 => {
  console.log(`${newSpeed1}`)
});

eventEmitter.emit("accelerate", 100);

不同点

EventEmitter事件订阅者之间是无感知的,相互无法影响的。WebpackTapable的事件订阅者之间即可以是无感知也可以是相互影响。

举个例子说明,比如SyncWaterfallHook中前一个订阅者的回调返回值会作为后一个订阅者的输入参数。

const swfh = new SyncWaterfallHook(['param']);

swfh.tap('a', function (param) {
  console.log(param);
  return param + 1;
});
swfh.tap('b', function (param) {
  console.log(param);
  return param + 2;
});
swfh.tap('c', function (param) {
  console.log(param);
});

swfh.call(1);

// console
/* 
   1
   2
   4
*/

不仅如此,Tapable还提供InterceptionContextHookMapMultiHook等玩法。