最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • tapable简单分析

    正文概述 掘金(lulu酱)   2021-02-26   613

    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里面。可以自行打印查看。


    起源地下载网 » tapable简单分析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元