tapable简单分析

993 阅读5分钟

tapable是webpack使用的事件处理模块,npm上可以看到一共提供了九种事件处理方式,分别是

SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook

这里选其中的几个研究一下

SyncHook

从lib/index.js点进去,该类代码只有数行:

const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}

const factory = new SyncHookCodeFactory();

const TAP_ASYNC = () => {
	throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
	throw new Error("tapPromise is not supported on a SyncHook");
};

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};

function SyncHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = SyncHook;
	hook.tapAsync = TAP_ASYNC;
	hook.tapPromise = TAP_PROMISE;
	hook.compile = COMPILE;
	return hook;
}

SyncHook.prototype = null;

module.exports = SyncHook;

实例化了一个Hook类,然后赋予tapAsync,tapPromise,compile函数。因为是同步钩子,所以tapAsync和tapPromise方法都只是抛出了一个错误。这里看不到call、tap方法,显然在Hook类里面。Hook代码如下:

...
class Hook {
	constructor(args = [], name = undefined) {
		this._args = args;
		this.name = name;
		this.taps = [];
		this.interceptors = [];
		this._call = CALL_DELEGATE;
		this.call = CALL_DELEGATE;
		this._callAsync = CALL_ASYNC_DELEGATE;
		this.callAsync = CALL_ASYNC_DELEGATE;
		this._promise = PROMISE_DELEGATE;
		this.promise = PROMISE_DELEGATE;
		this._x = undefined;

		this.compile = this.compile;
		this.tap = this.tap;
		this.tapAsync = this.tapAsync;
		this.tapPromise = this.tapPromise;
	}
	...
  
module.exports = Hook;

构造函数这里面最后四句我有点疑惑

this.compile = this.compile;
this.tap = this.tap;
this.tapAsync = this.tapAsync;
this.tapPromise = this.tapPromise;

说实话不知道有什么用,省去查找原型的时间?好像也没必要啊。从tap看起,tap里面调用了_tap,如下:

_tap(type, options, fn) {
  if (typeof options === "string") {
    options = {
      name: options.trim()
    };
  } else if (typeof options !== "object" || options === null) {
    throw new Error("Invalid tap options");
  }
  if (typeof options.name !== "string" || options.name === "") {
    throw new Error("Missing name for tap");
  }
  if (typeof options.context !== "undefined") {
    deprecateContext();
  }
  options = Object.assign({ type, fn }, options);
  console.log('option', options);
  options = this._runRegisterInterceptors(options);
  this._insert(options);
}

前面几行主要是生成option,从这里看到,tap的时候直接传入的第一个参数如果是个对象,它的fn属性是会覆盖第三个参数的。deprecateContext()是提醒context参数已经被取消了。然后执行_runRegisterInterceptors,如下:

_runRegisterInterceptors(options) {
  for (const interceptor of this.interceptors) {
    if (interceptor.register) {
      const newOptions = interceptor.register(options);
      if (newOptions !== undefined) {
        options = newOptions;
      }
    }
  }
  return options;
}

tapable的hook类提供intercept方法注册一些interceptors,_runRegisterInterceptors就是绑定监听(tap、tapPromise、tapAsync)的时候,把option依次交给所有的interceptors.register处理,最后生成一个新的option。最后使用_insert方法把option放到队列里:

_insert(item) {
		this._resetCompilation();
		let before;
		if (typeof item.before === "string") {
			before = new Set([item.before]);
		} else if (Array.isArray(item.before)) {
			before = new Set(item.before);
		}
		let stage = 0;
		if (typeof item.stage === "number") {
			stage = item.stage;
		}
		let i = this.taps.length;
		while (i > 0) {
			i--;
			const x = this.taps[i];
			this.taps[i + 1] = x;
			const xStage = x.stage || 0;
			if (before) {
				if (before.has(x.name)) {
					before.delete(x.name);
					continue;
				}
				if (before.size > 0) {
					continue;
				}
			}
			if (xStage > stage) {
				continue;
			}
			i++;
			break;
		}
		this.taps[i] = item;
	}
}

一开始先跑一次_resetCompilation,然后根据before和stage参数寻找位置插入到taps数组里面。x.name其实就是使用tap的时候的第一个参数:

hook.tap('name', function () {})
// 或者
hook.tap({
	name: 'name'
}, function () {})

before可以是字符串或则数组,最终插入的位置会在所有before的项的前面。stage参数则是越大排越前面。为什么要跑_resetCompilation呢?_resetCompilation代码如下:

_resetCompilation() {
  this.call = this._call;
  this.callAsync = this._callAsync;
  this.promise = this._promise;
}
const CALL_DELEGATE = function(...args) {
	this.call = this._createCall("sync");
	return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function(...args) {
	this.callAsync = this._createCall("async");
	return this.callAsync(...args);
};
const PROMISE_DELEGATE = function(...args) {
	this.promise = this._createCall("promise");
	return this.promise(...args);
};

this._callthis._callAsyncthis._promise在构造函数中和this.callthis.callAsyncthis.promise一样,都等于CALL_DELEGATECALL_ASYNC_DELEGATEPROMISE_DELEGATE。当调用call等方法,this.call会被重新赋值。_createCall的作用就是根据传入的参数生成一个函数,如下:

_createCall(type) {
  return this.compile({
    taps: this.taps,
    interceptors: this.interceptors,
    args: this._args,
    type: type
  });
}
compile(options) {
  throw new Error("Abstract: should be overridden");
}

compile是个抽象方法,具体的实现在子类,也就是最开头的那段代码。调用了

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};

很显然,重新调用的原因就是因为这个工厂方法create会根据参数生成不同的处理逻辑,所以每次tap都要更新一次call函数。

为啥要重新生成call呢?在我的想法里,tap的时候把fn都放到一个list里面,然后call的时候逐个判断触发,所以call是不需要改变的。查阅后发现,call的执行代码是动态生成的,根据webpack成员的回复,是为了能更快的执行 github.com/webpack/tap…

看下factory.create的代码:

create(options) {
  this.init(options);
  let fn;
  switch (this.options.type) {
    case "sync":
      fn = new Function(
        this.args(),
        '"use strict";\n' +
        this.header() +
        this.contentWithInterceptors({
          onError: err => `throw ${err};\n`,
          onResult: result => `return ${result};\n`,
          resultReturns: true,
          onDone: () => "",
          rethrowIfPossible: true
        })
      );
      break;
    case "async":
      ...
    case "promise":
      ...
  }
  this.deinit();
  return fn;
}
deinit() {
  this.options = undefined;
  this._args = undefined;
}

使用new Function的方式生成一个新函数,先用this.args函数将参数用逗号隔开,这里的参数就是指new Hook(['param1', 'param2'])里面的['param1', 'param2']。然后用this.header()在代码开头声明一些变量,如下:

header() {
  let code = "";
  if (this.needContext()) {
    code += "var _context = {};\n";
  } else {
    code += "var _context;\n";
  }
  code += "var _x = this._x;\n";
  if (this.options.interceptors.length > 0) {
    code += "var _taps = this.taps;\n";
    code += "var _interceptors = this.interceptors;\n";
  }
  return code;
}
needContext() {
	for (const tap of this.options.taps) if (tap.context) return true;
	return false;
}

其中this._x在调用COMPILE的时候在factory.setup里赋值

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

上面只是一些变量的初始化。继续看contentWithInterceptors:

contentWithInterceptors(options) {
  if (this.options.interceptors.length > 0) {
    const onError = options.onError;
    const onResult = options.onResult;
    const onDone = options.onDone;
    let code = "";
    for (let i = 0; i < this.options.interceptors.length; i++) {
      const interceptor = this.options.interceptors[i];
      if (interceptor.call) {
        code += `${this.getInterceptor(i)}.call(${this.args({
          before: interceptor.context ? "_context" : undefined
        })});\n`;
      }
    }
    code += this.content(
      Object.assign(options, {
        onError:
        onError &&
        (err => {
          let code = "";
          for (let i = 0; i < this.options.interceptors.length; i++) {
            const interceptor = this.options.interceptors[i];
            if (interceptor.error) {
              code += `${this.getInterceptor(i)}.error(${err});\n`;
            }
          }
          code += onError(err);
          return code;
        }),
        onResult:
        onResult &&
        (result => {
          let code = "";
          for (let i = 0; i < this.options.interceptors.length; i++) {
            const interceptor = this.options.interceptors[i];
            if (interceptor.result) {
              code += `${this.getInterceptor(i)}.result(${result});\n`;
            }
          }
          code += onResult(result);
          return code;
        }),
        onDone:
        onDone &&
        (() => {
          let code = "";
          for (let i = 0; i < this.options.interceptors.length; i++) {
            const interceptor = this.options.interceptors[i];
            if (interceptor.done) {
              code += `${this.getInterceptor(i)}.done();\n`;
            }
          }
          code += onDone();
          return code;
        })
      })
    );
    return code;
  } else {
    return this.content(options);
  }
}

第一段代码拼接先加了拦截器call方法的调用,第二次拼接了this.content(options)的结果,content是子类上声明的方法:

content({ onError, onDone, rethrowIfPossible }) {
  var code = this.callTapsSeries({
    onError: (i, err) => onError(err),
    onDone,
    rethrowIfPossible
  });
  return code;
}

callTapsSeries 后面就是根据taps数组开始拼凑代码。因为拼凑的过程我也看不懂,我这里直接看拼出来的代码:

// 源码
var tapable = require('tapable');
class Person {
  constructor (name) {
    this.name = name;
    this.hooks = {
      intro: new tapable.SyncHook(['name'])
    }
  }
}
var man = new Person('lujiajian');
man.hooks.intro.tap('introduce', (name) => {
  console.log('tap:' + name);
});
man.hooks.intro.tap('introduce2', (name) => {
  console.log('tap2:' + name);
});
man.hooks.intro.intercept({
  call: (source, target, routesList) => {
		console.log("Starting to calculate routes");
	},
	register: (tapInfo) => {
		console.log(`${tapInfo.name} is doing its job`);
		return tapInfo; // may return a new tapInfo object
	}
})
man.hooks.intro.call('lujiajian');
// sync执行函数
"use strict";
var _context;
var _x = this._x;
var _taps = this.taps;
var _interceptors = this.interceptors;
_interceptors[0].call(name);
var _fn0 = _x[0];
_fn0(name);
var _fn1 = _x[1];
_fn1(name);

直接拼出来的代码不需要用for!

SyncBailHook

因为大部分都是一样的,也是直接看拼出来的代码。

// 源码
var tapable = require('tapable');
class Person {
  constructor (name) {
    this.name = name;
    this.hooks = {
      intro: new tapable.SyncHook(['name'])
    }
  }
}
var man = new Person('lujiajian');
man.hooks.intro.tap('introduce', (name) => {
  console.log('tap:' + name);
});
man.hooks.intro.tap('introduce2', (name) => {
  console.log('tap2:' + name);
});
man.hooks.intro.intercept({
  call: (source, target, routesList) => {
		console.log("Starting to calculate routes");
	},
	register: (tapInfo) => {
		console.log(`${tapInfo.name} is doing its job`);
		return tapInfo; // may return a new tapInfo object
	}
})
man.hooks.intro.call('lujiajian');
// sync执行函数
"use strict";
var _context;
var _x = this._x;
var _taps = this.taps;
var _interceptors = this.interceptors;
_interceptors[0].call(name);
var _fn0 = _x[0];
var _result0 = _fn0(name);
if (_result0 !== undefined) {
  return _result0;;
} else {
  var _fn1 = _x[1];
  var _result1 = _fn1(name);
  if (_result1 !== undefined) {
    return _result1;;
  } else {}
}

就是一直用else if嵌套下去,返回值非undefined就打断。

SyncWaterfallHook

// 源码
var tapable = require('tapable');
class Person {
  constructor (name) {
    this.name = name;
    this.hooks = {
      intro: new tapable.SyncWaterfallHook(['name', 'name2'])
    }
  }
}
var man = new Person('lujiajian');

man.hooks.intro.tap('introduce', (name1, name2) => {
  console.log('tap:' + name1 + name2);
});
man.hooks.intro.tap('introduce2', (name1, name2) => {
  console.log('tap2:' + name1 + name2);
});
man.hooks.intro.intercept({
  call: (source, target, routesList) => {
		console.log("Starting to calculate routes");
	},
	register: (tapInfo) => {
		// tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
		console.log(`${tapInfo.name} is doing its job`);
		return tapInfo; // may return a new tapInfo object
	}
})

man.hooks.intro.call('lujiajian1', 'lujiajian2');

// sync执行函数
"use strict";
var _context;
var _x = this._x;
var _taps = this.taps;
var _interceptors = this.interceptors;
_interceptors[0].call(name, name2);
var _fn0 = _x[0];
var _result0 = _fn0(name, name2);
if (_result0 !== undefined) {
  name = _result0;
}
var _fn1 = _x[1];
var _result1 = _fn1(name, name2);
if (_result1 !== undefined) {
  name = _result1;
}
return name;

因为好奇他返回值的传递,参数比上面的例子多一个。结果发现name2参数貌似根本改变不了。一直只能改第一个。

AsyncSeriesWaterfallHook

// 源码
var tapable = require('tapable');
class Person {
  constructor (name) {
    this.name = name;
    this.hooks = {
      intro: new tapable.AsyncSeriesWaterfallHook(['name', 'name2'])
    }
  }
}
var man = new Person('lujiajian');

man.hooks.intro.tapAsync('introduce', (name1, name2, cb) => {
  console.log('tap:' + name1 + name2);
  cb()
});
man.hooks.intro.tapPromise('introduce2', (name1, name2) => {
  console.log('tap2:' + name1 + name2);
  return Promise.resolve();
});
man.hooks.intro.intercept({
  call: (source, target, routesList) => {
		console.log("Starting to calculate routes");
	},
	register: (tapInfo) => {
		// tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
		console.log(`${tapInfo.name} is doing its job`);
		return tapInfo; // may return a new tapInfo object
	}
})

man.hooks.intro.promise('lujiajian1', 'lujiajian2');
// promise执行代码
"use strict";
var _context;
var _x = this._x;
var _taps = this.taps;
var _interceptors = this.interceptors;
return new Promise((function (_resolve, _reject) {
  var _sync = true;

  function _error(_err) {
    if (_sync)
      _resolve(Promise.resolve().then((function () {
        throw _err;
      })));
    else
      _reject(_err);
  };
  _interceptors[0].call(name, name2);

  function _next0() {
    var _fn1 = _x[1];
    var _hasResult1 = false;
    var _promise1 = _fn1(name, name2);
    if (!_promise1 || !_promise1.then)
      throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise1 + ')');
    _promise1.then((function (_result1) {
      _hasResult1 = true;
      if (_result1 !== undefined) {
        name = _result1;
      }
      _resolve(name);
    }), function (_err1) {
      if (_hasResult1) throw _err1;
      _error(_err1);
    });
  }
  var _fn0 = _x[0];
  _fn0(name, name2, (function (_err0, _result0) {
    if (_err0) {
      _error(_err0);
    } else {
      if (_result0 !== undefined) {
        name = _result0;
      }
      _next0();
    }
  }));
  _sync = false;
}));

Async绑定的函数是用一个_next{index}函数包裹起来的, callback的时候执行下一个next函数,promise好像只是在后面加了一些错误处理。所以用tapAsync的时候,callback必须要传参,不然运行报错。用tapPromise的时候,必须返回一个promise,不然会抛出错误。

其他的就不再打印出来了,代码都在factory.create里面。可以自行打印查看。