tapable源码分析
webpack 事件處理機制Tapable
webpack 的諸多核心模塊都是tapable的子類, tapable提供了一套完整事件訂閱和發布的機制,讓webpack的執行的流程交給了訂閱的插件去處理, 當然這套機制為開發者訂閱流程事件去定制自己構建模式和方案提供了更多的便利性, 從基礎的原理角度說,tapable就是一套觀察者模式,并在此基礎上提供了較為豐富的訂閱和發布方式,如 call/async /promise,以此支持更多的處理場景。
- 以下是tapable 提供的所有的hook類型
hook 分析
hook 主要分一下類型 async / sync
上圖來自這里
總體介紹
在正式分析源碼之前,先對每一種hook的進行功能介紹和簡單源碼分析
| 1 | SyncHook | 同步串行 | 不關心監聽函數的返回值 |
| 2 | SyncBailHook | 同步串行 | 只要監聽函數中有一個函數的返回值不為 null,則跳過剩下所有的邏輯 |
| 3 | SyncWaterfallHook | 同步串行 | 上一個監聽函數的返回值可以傳給下一個監聽函數 |
| 4 | SyncLoopHook | 同步循環 | 當監聽函數被觸發的時候,如果該監聽函數返回true時則這個監聽函數會反復執行,如果返回 undefined 則表示退出循環 |
| 5 | AsyncParallelHook | 異步并發 | 不關心監聽函數的返回值 |
| 6 | AsyncParallelBailHook | 異步并發 | 只要監聽函數的返回值不為 null,就會忽略后面的監聽函數執行,直接跳躍到callAsync等觸發函數綁定的回調函數,然后執行這個被綁定的回調函數 |
| 7 | AsyncSeriesHook | 異步串行 | 不關心callback()的參數 |
| 8 | AsyncSeriesBailHook | 異步串行 | callback()的參數不為null,就會直接執行callAsync等觸發函數綁定的回調函數 |
| 9 | AsyncSeriesWaterfallHook | 異步串行 | 上一個監聽函數的中的callback(err, data)的第二個參數,可以作為下一個監聽函數的參數 |
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就是很簡單的訂閱 同步發布 不關心訂閱函數返回值, 一口氣把把所有訂閱者執行一遍
原理 就是簡單的訂閱和發布
class SyncHook{constructor(){this.subs = [];}tap(fn){this.sub.push(fn);}call(...args){this.subs.forEach(fn=>fn(...args));} } 復制代碼遇到訂閱函數返回不為空的情況下 就會停止執行剩余的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;} } 復制代碼
上一個監聽函數的返回值可以傳給下一個監聽函數
原理: 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); }} } 復制代碼
當監聽函數被觸發的時候,如果該監聽函數返回true時則這個監聽函數會反復執行,如果返回 undefined 則表示退出循環
原理 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(); 復制代碼結論:
- hook 不在乎callback的返回值
- callback 第一個參數給給值 表示異常 就會結束
- 監聽函數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 實現的思路是: 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;} 復制代碼- taps 訂閱函數集合
- interceptors 攔截集合 配置在執行派發之前的攔截
- call 同步觸發對象
- promise promise方式觸發的對象
- callSync 異步觸發對象
- _x 應用于生成執行函數 監聽集合 后續詳細介紹這個_x 的使用意義 (這個名字起得有點怪)
既然hook是觀察者模式實現的,我們就順著觀察者模式的思路去逐步解析hook的實現方法
hook之訂閱
- 同步訂閱 tap 方法
- _insert 方法
insert 依賴方法
_resetCompilation
// 定義方法委托 讓hook[name]方法 = hook._createCall(type) 產生 // sync 調用call // promise 調用promise // async 調用 callAsync function createCompileDelegate(name, type) {return function lazyCompileHook(...args) {// 這里很妙 將生成函數的指向上下文 給定給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; }復制代碼// 根據源碼可以知道 重置就是讓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的位置 // 如果監聽函數沒有配置這兩個參數就會執行 this.taps[i] = item // 最后位置保存新加入的訂閱 從此完成訂閱//如果配置了before 就會移動位置 根據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;}//保存新監聽函數this.taps[i] = item;} 復制代碼
在上面分析到的 call/callAsync/promise 方法中 使用到了createCall 方法和 _compile
- 異步訂閱tapAsync
- 異步訂閱 tapPromise
// 其實源碼訂閱方法有重構的空間 好多代碼冗余
hook之發布
// 這個方法交給子類自己實現 也就是說 怎么發布訂閱由子類自己實現 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 沒有做任何處理 ? 帶著這個問題我們去分析
也就是說hook將自己的發布交給了子類去實現
HookCodeFactory
hook的發布方法(compile)交給 子類自己去實現,同時提供了代碼組裝工程類,這個類為所有類別的hook的提供了代碼生成基礎方法,下面我們詳細分析這個類的代碼組成
HookCodeFactory 最終生成可執行的代碼片段和普通的模板編譯方法差不多
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 監聽函數的集合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的參數this._args = options.args.slice();}deinit() {this.options = undefined;this._args = undefined;}// 代碼header部分// 這里定義了 _X的值// interceptors 的執行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;}// 觸發訂閱/*** 構建發布方法* 分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 = "";// 回調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;}// 調用串行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);}// 觸發循環調用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;}//生成參數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 官方源碼
轉載于:https://juejin.im/post/5c25d6706fb9a049a81f6488
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的tapable源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一个数据库存储架构的独白
- 下一篇: 400 错误,因为url编码问题