tapable源码分析
webpack 事件處理機制Tapable
webpack 的諸多核心模塊都是tapable的子類, tapable提供了一套完整事件訂閱和發(fā)布的機制,讓webpack的執(zhí)行的流程交給了訂閱的插件去處理, 當然這套機制為開發(fā)者訂閱流程事件去定制自己構(gòu)建模式和方案提供了更多的便利性, 從基礎的原理角度說,tapable就是一套觀察者模式,并在此基礎上提供了較為豐富的訂閱和發(fā)布方式,如 call/async /promise,以此支持更多的處理場景。
- 以下是tapable 提供的所有的hook類型
hook 分析
hook 主要分一下類型 async / sync
上圖來自這里
總體介紹
在正式分析源碼之前,先對每一種hook的進行功能介紹和簡單源碼分析
| 1 | SyncHook | 同步串行 | 不關(guān)心監(jiān)聽函數(shù)的返回值 |
| 2 | SyncBailHook | 同步串行 | 只要監(jiān)聽函數(shù)中有一個函數(shù)的返回值不為 null,則跳過剩下所有的邏輯 |
| 3 | SyncWaterfallHook | 同步串行 | 上一個監(jiān)聽函數(shù)的返回值可以傳給下一個監(jiān)聽函數(shù) |
| 4 | SyncLoopHook | 同步循環(huán) | 當監(jiān)聽函數(shù)被觸發(fā)的時候,如果該監(jiān)聽函數(shù)返回true時則這個監(jiān)聽函數(shù)會反復執(zhí)行,如果返回 undefined 則表示退出循環(huán) |
| 5 | AsyncParallelHook | 異步并發(fā) | 不關(guān)心監(jiān)聽函數(shù)的返回值 |
| 6 | AsyncParallelBailHook | 異步并發(fā) | 只要監(jiān)聽函數(shù)的返回值不為 null,就會忽略后面的監(jiān)聽函數(shù)執(zhí)行,直接跳躍到callAsync等觸發(fā)函數(shù)綁定的回調(diào)函數(shù),然后執(zhí)行這個被綁定的回調(diào)函數(shù) |
| 7 | AsyncSeriesHook | 異步串行 | 不關(guān)心callback()的參數(shù) |
| 8 | AsyncSeriesBailHook | 異步串行 | callback()的參數(shù)不為null,就會直接執(zhí)行callAsync等觸發(fā)函數(shù)綁定的回調(diào)函數(shù) |
| 9 | AsyncSeriesWaterfallHook | 異步串行 | 上一個監(jiān)聽函數(shù)的中的callback(err, data)的第二個參數(shù),可以作為下一個監(jiān)聽函數(shù)的參數(shù) |
Demo驗證
1 sync
const {SyncHook,SyncBailHook,SyncLoopHook,SyncWaterfallHook } = require('tapable')class SyncHookDemo{constructor(){this.hooks = {sh: new SyncHook(['name', 'age']),sbh: new SyncBailHook(['name', 'age']),slh: new SyncLoopHook(['name']),swh: new SyncWaterfallHook(['name', 'nickname', 'user'])}} }const hdemo = new SyncHookDemo();復制代碼synchook就是很簡單的訂閱 同步發(fā)布 不關(guān)心訂閱函數(shù)返回值, 一口氣把把所有訂閱者執(zhí)行一遍
原理 就是簡單的訂閱和發(fā)布
class SyncHook{constructor(){this.subs = [];}tap(fn){this.sub.push(fn);}call(...args){this.subs.forEach(fn=>fn(...args));} } 復制代碼遇到訂閱函數(shù)返回不為空的情況下 就會停止執(zhí)行剩余的callback
原理 class SyncBailHook{constructor(){this.subs = [];}tap(fn){this.sub.push(fn);}call(...args){for(let i=0; i< this.subs.length; i++){const result = this.subs[i](...args);result && break;} } 復制代碼
上一個監(jiān)聽函數(shù)的返回值可以傳給下一個監(jiān)聽函數(shù)
原理: class SyncWaterHook{constructor(){this.subs = [];}tap(fn){this.sub.push(fn);}call(...args){let result = null;for(let i = 0, l = this.hooks.length; i < l; i++) {let hook = this.hooks[i];result = i == 0 ? hook(...args): hook(result); }} } 復制代碼
當監(jiān)聽函數(shù)被觸發(fā)的時候,如果該監(jiān)聽函數(shù)返回true時則這個監(jiān)聽函數(shù)會反復執(zhí)行,如果返回 undefined 則表示退出循環(huán)
原理 class SyncLooHook{constructor(){this.subs = [];}tap(fn){this.sub.push(fn);}call(...args){let result;do {result = this.hook(...arguments);} while (result!==undefined)} } 復制代碼
2. async
const {AsyncParallelHook,AsyncParallelBailHook,AsyncSeriesHook,AsyncSeriesBailHook,AsyncSeriesWaterfallHook} = require('tapable')class AsyncHookDemo{constructor(){this.hooks = {aph: new AsyncParallelHook(['name']),apbh: new AsyncParallelBailHook(['name']),ash: new AsyncSeriesHook(['name']),asbh: new AsyncSeriesBailHook(['name']),aswh: new AsyncSeriesWaterfallHook(['name'])}}}const shdemo = new AsyncHookDemo(); 復制代碼結(jié)論:
- hook 不在乎callback的返回值
- callback 第一個參數(shù)給給值 表示異常 就會結(jié)束
- 監(jiān)聽函數(shù)throw 一個異常會被最后的callback捕獲
可能看起來有點懵, 為什么是這樣,我們還是從源碼入手,看看各類hook源碼
然后在demo驗證 分析的對否
如果想自己想試試的,可以直接使用參考的資料的第一個鏈接,
先從基類
Hook源代碼
;class Hook {constructor(args) {if (!Array.isArray(args)) args = [];this._args = args;this.taps = [];this.interceptors = [];this.call = this._call;this.promise = this._promise;this.callAsync = this._callAsync;this._x = undefined;}compile(options) {throw new Error("Abstract: should be overriden");}_createCall(type) {return this.compile({taps: this.taps,interceptors: this.interceptors,args: this._args,type: type});}tap(options, fn) {if (typeof options === "string") options = { name: options };if (typeof options !== "object" || options === null)throw new Error("Invalid arguments to tap(options: Object, fn: function)");options = Object.assign({ type: "sync", fn: fn }, options);if (typeof options.name !== "string" || options.name === "")throw new Error("Missing name for tap");options = this._runRegisterInterceptors(options);this._insert(options);}tapAsync(options, fn) {if (typeof options === "string") options = { name: options };if (typeof options !== "object" || options === null)throw new Error("Invalid arguments to tapAsync(options: Object, fn: function)");options = Object.assign({ type: "async", fn: fn }, options);if (typeof options.name !== "string" || options.name === "")throw new Error("Missing name for tapAsync");options = this._runRegisterInterceptors(options);this._insert(options);}tapPromise(options, fn) {if (typeof options === "string") options = { name: options };if (typeof options !== "object" || options === null)throw new Error("Invalid arguments to tapPromise(options: Object, fn: function)");options = Object.assign({ type: "promise", fn: fn }, options);if (typeof options.name !== "string" || options.name === "")throw new Error("Missing name for tapPromise");options = this._runRegisterInterceptors(options);this._insert(options);}_runRegisterInterceptors(options) {for (const interceptor of this.interceptors) {if (interceptor.register) {const newOptions = interceptor.register(options);if (newOptions !== undefined) options = newOptions;}}return options;}withOptions(options) {const mergeOptions = opt =>Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);// Prevent creating endless prototype chainsoptions = Object.assign({}, options, this._withOptions);const base = this._withOptionsBase || this;const newHook = Object.create(base);(newHook.tapAsync = (opt, fn) => base.tapAsync(mergeOptions(opt), fn)),(newHook.tap = (opt, fn) => base.tap(mergeOptions(opt), fn));newHook.tapPromise = (opt, fn) => base.tapPromise(mergeOptions(opt), fn);newHook._withOptions = options;newHook._withOptionsBase = base;return newHook;}isUsed() {return this.taps.length > 0 || this.interceptors.length > 0;}intercept(interceptor) {this._resetCompilation();this.interceptors.push(Object.assign({}, interceptor));if (interceptor.register) {for (let i = 0; i < this.taps.length; i++)this.taps[i] = interceptor.register(this.taps[i]);}}_resetCompilation() {this.call = this._call;this.callAsync = this._callAsync;this.promise = this._promise;}_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;} }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},_promise: {value: createCompileDelegate("promise", "promise"),configurable: true,writable: true},_callAsync: {value: createCompileDelegate("callAsync", "async"),configurable: true,writable: true} }); 復制代碼hook 實現(xiàn)的思路是: hook 使用觀察者模式,構(gòu)造函數(shù)需要提供一個參數(shù)數(shù)組,就是派發(fā)事件的參數(shù)集合
constructor(args) {if (!Array.isArray(args)) args = [];this._args = args;this.taps = [];this.interceptors = [];this.call = this._call;this.promise = this._promise;this.callAsync = this._callAsync;this._x = undefined;} 復制代碼- taps 訂閱函數(shù)集合
- interceptors 攔截集合 配置在執(zhí)行派發(fā)之前的攔截
- call 同步觸發(fā)對象
- promise promise方式觸發(fā)的對象
- callSync 異步觸發(fā)對象
- _x 應用于生成執(zhí)行函數(shù) 監(jiān)聽集合 后續(xù)詳細介紹這個_x 的使用意義 (這個名字起得有點怪)
既然hook是觀察者模式實現(xiàn)的,我們就順著觀察者模式的思路去逐步解析hook的實現(xiàn)方法
hook之訂閱
- 同步訂閱 tap 方法
- _insert 方法
insert 依賴方法
_resetCompilation
// 定義方法委托 讓hook[name]方法 = hook._createCall(type) 產(chǎn)生 // sync 調(diào)用call // promise 調(diào)用promise // async 調(diào)用 callAsync function createCompileDelegate(name, type) {return function lazyCompileHook(...args) {// 這里很妙 將生成函數(shù)的指向上下文 給定給this 也就是說 模板代碼中// this 可以獲取hook的屬性和方法 this._x 就可以排上用途了 具體的在 //factory 里面分析this[name] = this._createCall(type);return this[name](...args);}; } Object.defineProperties(Hook.prototype, {_call: {value: createCompileDelegate("call", "sync"),configurable: true,writable: true},_promise: {value: createCompileDelegate("promise", "promise"),configurable: true,writable: true},_callAsync: {value: createCompileDelegate("callAsync", "async"),configurable: true,writable: true} });// 重置 call / callAsync promise // 為什么要重置_resetCompilation() {this.call = this._call;this.callAsync = this._callAsync;this.promise = this._promise; }復制代碼// 根據(jù)源碼可以知道 重置就是讓call/callAsync/promise方法來自原型上的 _call/_promise/_callAsync 同時賦值
insert 源碼 _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);}// 通過item 如果配置了before 和 stage 來控制item在 taps的位置 // 如果監(jiān)聽函數(shù)沒有配置這兩個參數(shù)就會執(zhí)行 this.taps[i] = item // 最后位置保存新加入的訂閱 從此完成訂閱//如果配置了before 就會移動位置 根據(jù)name值 將item放在相應的位置//stage 同理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;}//保存新監(jiān)聽函數(shù)this.taps[i] = item;} 復制代碼
在上面分析到的 call/callAsync/promise 方法中 使用到了createCall 方法和 _compile
- 異步訂閱tapAsync
- 異步訂閱 tapPromise
// 其實源碼訂閱方法有重構(gòu)的空間 好多代碼冗余
hook之發(fā)布
// 這個方法交給子類自己實現(xiàn) 也就是說 怎么發(fā)布訂閱由子類自己實現(xiàn) compile(options) {throw new Error("Abstract: should be overriden");}_createCall(type) {return this.compile({taps: this.taps,interceptors: this.interceptors,args: this._args,type: type}); } 復制代碼問題 ?
this._x 沒有做任何處理 ? 帶著這個問題我們?nèi)シ治?/p>
也就是說hook將自己的發(fā)布交給了子類去實現(xiàn)
HookCodeFactory
hook的發(fā)布方法(compile)交給 子類自己去實現(xiàn),同時提供了代碼組裝工程類,這個類為所有類別的hook的提供了代碼生成基礎方法,下面我們詳細分析這個類的代碼組成
HookCodeFactory 最終生成可執(zhí)行的代碼片段和普通的模板編譯方法差不多
class HookCodeFactory {/*** config 配置options = 就是{taps: this.taps,interceptors: this.interceptors,args: this._args,type: type}*/constructor(config) {this.config = config;this.options = undefined;this._args = undefined;}/*** options = {taps: this.taps,interceptors: this.interceptors,args: this._args,type: type}*/create(options) {this.init(options);let fn;switch (this.options.type) {case "sync":// 同步代碼模板 fn = new Function(this.args(),'"use strict";\n' +this.header() +this.content({onError: err => `throw ${err};\n`,onResult: result => `return ${result};\n`,onDone: () => "",rethrowIfPossible: true}));break;case "async":// 異步代碼模板fn = new Function(this.args({after: "_callback"}),'"use strict";\n' +this.header() +this.content({onError: err => `_callback(${err});\n`,onResult: result => `_callback(null, ${result});\n`,onDone: () => "_callback();\n"}));break;case "promise":// promise代碼模板let code = "";code += '"use strict";\n';code += "return new Promise((_resolve, _reject) => {\n";code += "var _sync = true;\n";code += this.header();code += this.content({onError: err => {let code = "";code += "if(_sync)\n";code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;code += "else\n";code += `_reject(${err});\n`;return code;},onResult: result => `_resolve(${result});\n`,onDone: () => "_resolve();\n"});code += "_sync = false;\n";code += "});\n";fn = new Function(this.args(), code);break;}// 重置 options和argsthis.deinit();return fn;}setup(instance, options) {// 安裝實例 讓模板代碼里的this._x 給與值 // 這里可以解釋 hook源碼中 定義未賦值_x的疑問了// _x 其實就是taps 監(jiān)聽函數(shù)的集合instance._x = options.taps.map(t => t.fn);}/*** @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options*/init(options) {//賦值this.options = options;// 賦值args的參數(shù)this._args = options.args.slice();}deinit() {this.options = undefined;this._args = undefined;}// 代碼header部分// 這里定義了 _X的值// interceptors 的執(zhí)行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";}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`;}}return code;}needContext() {for (const tap of this.options.taps) if (tap.context) return true;return false;}// 觸發(fā)訂閱/*** 構(gòu)建發(fā)布方法* 分sync async promise*/callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {let code = "";let hasTapCached = false;for (let i = 0; i < this.options.interceptors.length; i++) {const interceptor = this.options.interceptors[i];if (interceptor.tap) {if (!hasTapCached) {code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;hasTapCached = true;}code += `${this.getInterceptor(i)}.tap(${interceptor.context ? "_context, " : ""}_tap${tapIndex});\n`;}}code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;const tap = this.options.taps[tapIndex];switch (tap.type) {case "sync":// 捕獲異常if (!rethrowIfPossible) {code += `var _hasError${tapIndex} = false;\n`;code += "try {\n";}// 是否有返回值if (onResult) {code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({before: tap.context ? "_context" : undefined})});\n`;} else {code += `_fn${tapIndex}(${this.args({before: tap.context ? "_context" : undefined})});\n`;}if (!rethrowIfPossible) {code += "} catch(_err) {\n";code += `_hasError${tapIndex} = true;\n`;code += onError("_err");code += "}\n";code += `if(!_hasError${tapIndex}) {\n`;}if (onResult) {code += onResult(`_result${tapIndex}`);}// 完成if (onDone) {code += onDone();}if (!rethrowIfPossible) {code += "}\n";}break;case "async":let cbCode = "";// 回調(diào)if (onResult) cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;else cbCode += `_err${tapIndex} => {\n`;cbCode += `if(_err${tapIndex}) {\n`;cbCode += onError(`_err${tapIndex}`);cbCode += "} else {\n";if (onResult) {cbCode += onResult(`_result${tapIndex}`);}if (onDone) {cbCode += onDone();}cbCode += "}\n";cbCode += "}";code += `_fn${tapIndex}(${this.args({before: tap.context ? "_context" : undefined,after: cbCode})});\n`;break;case "promise":code += `var _hasResult${tapIndex} = false;\n`;code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({before: tap.context ? "_context" : undefined})});\n`;// 需要返回promise code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;code += ` throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`;code += `_promise${tapIndex}.then(_result${tapIndex} => {\n`;code += `_hasResult${tapIndex} = true;\n`;if (onResult) {code += onResult(`_result${tapIndex}`);}if (onDone) {code += onDone();}code += `}, _err${tapIndex} => {\n`;code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;code += onError(`_err${tapIndex}`);code += "});\n";break;}return code;}// 調(diào)用串行callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {if (this.options.taps.length === 0) return onDone();const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");const next = i => {if (i >= this.options.taps.length) {return onDone();}const done = () => next(i + 1);const doneBreak = skipDone => {if (skipDone) return "";return onDone();};return this.callTap(i, {onError: error => onError(i, error, done, doneBreak),onResult:onResult &&(result => {return onResult(i, result, done, doneBreak);}),onDone:!onResult &&(() => {return done();}),rethrowIfPossible:rethrowIfPossible && (firstAsync < 0 || i < firstAsync)});};return next(0);}// 觸發(fā)循環(huán)調(diào)用callTapsLooping({ onError, onDone, rethrowIfPossible }) {if (this.options.taps.length === 0) return onDone();const syncOnly = this.options.taps.every(t => t.type === "sync");let code = "";if (!syncOnly) {code += "var _looper = () => {\n";code += "var _loopAsync = false;\n";}code += "var _loop;\n";code += "do {\n";code += "_loop = false;\n";for (let i = 0; i < this.options.interceptors.length; i++) {const interceptor = this.options.interceptors[i];if (interceptor.loop) {code += `${this.getInterceptor(i)}.loop(${this.args({before: interceptor.context ? "_context" : undefined})});\n`;}}code += this.callTapsSeries({onError,onResult: (i, result, next, doneBreak) => {let code = "";code += `if(${result} !== undefined) {\n`;code += "_loop = true;\n";if (!syncOnly) code += "if(_loopAsync) _looper();\n";code += doneBreak(true);code += `} else {\n`;code += next();code += `}\n`;return code;},onDone:onDone &&(() => {let code = "";code += "if(!_loop) {\n";code += onDone();code += "}\n";return code;}),rethrowIfPossible: rethrowIfPossible && syncOnly});code += "} while(_loop);\n";if (!syncOnly) {code += "_loopAsync = true;\n";code += "};\n";code += "_looper();\n";}return code;}// 并行callTapsParallel({onError,onResult,onDone,rethrowIfPossible,onTap = (i, run) => run()}) {if (this.options.taps.length <= 1) {return this.callTapsSeries({onError,onResult,onDone,rethrowIfPossible});}let code = "";code += "do {\n";code += `var _counter = ${this.options.taps.length};\n`;if (onDone) {code += "var _done = () => {\n";code += onDone();code += "};\n";}for (let i = 0; i < this.options.taps.length; i++) {const done = () => {if (onDone) return "if(--_counter === 0) _done();\n";else return "--_counter;";};const doneBreak = skipDone => {if (skipDone || !onDone) return "_counter = 0;\n";else return "_counter = 0;\n_done();\n";};code += "if(_counter <= 0) break;\n";code += onTap(i,() =>this.callTap(i, {onError: error => {let code = "";code += "if(_counter > 0) {\n";code += onError(i, error, done, doneBreak);code += "}\n";return code;},onResult:onResult &&(result => {let code = "";code += "if(_counter > 0) {\n";code += onResult(i, result, done, doneBreak);code += "}\n";return code;}),onDone:!onResult &&(() => {return done();}),rethrowIfPossible}),done,doneBreak);}code += "} while(false);\n";return code;}//生成參數(shù)args({ before, after } = {}) {let allArgs = this._args;if (before) allArgs = [before].concat(allArgs);if (after) allArgs = allArgs.concat(after);if (allArgs.length === 0) {return "";} else {return allArgs.join(", ");}}getTapFn(idx) {return `_x[${idx}]`;}getTap(idx) {return `_taps[${idx}]`;}getInterceptor(idx) {return `_interceptors[${idx}]`;} } 復制代碼有點長 下一篇具體分析每一種類型的hook 暫時先分析這么多
參考資料
- webpack4.0源碼分析之Tapable
- tapable 官方源碼
轉(zhuǎn)載于:https://juejin.im/post/5c25d6706fb9a049a81f6488
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的tapable源码分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一个数据库存储架构的独白
- 下一篇: 400 错误,因为url编码问题