Webpack Hook杂谈

Tapable

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

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

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

  1. Hook
  2. HookCodeFactory

下面以SyncHook为例,咱们看看Hook处理的整个流程。SyncHook是Tapable中最容易理解的Hook,所以做为Demo进行分析。webpack

Demo代码以下:git

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方法(代码通过部分删减和转换)。github

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数组中。web

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

{
    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等玩法。

相关文章
相关标签/搜索