摘要:源碼分析安裝好包,根據上述方法,我們運行如下命令初始化在構造函數處打上斷點,可以看到繼承自,上面定義了一個函數。因為函數定義在原型上,并通過在構造函數中賦值。
Webpack源碼閱讀之Tapable
webpack采用Tapable來進行流程控制,在這套體系上,內部近百個插件有條不紊,還能支持外部開發自定義插件來擴展功能,所以在閱讀webpack源碼前先了解Tapable的機制是很有必要的。
Tapable的基本使用方法就不介紹了,可以參考官方文檔
https://github.com/webpack/ta...
1. 例子從網上拷貝了一個簡單的使用例子:
//main.js const { SyncHook } = require("tapable") //創建一個簡單的同步串行鉤子 let h1 = new SyncHook(["arg1,arg2"]); //在鉤子上添加訂閱者,鉤子被call時會觸發訂閱的回調函數 h1.tap("A",function(arg){ console.log("A",arg); return "b" }) h1.tap("B",function(){ console.log("b") }) h1.tap("C",function(){ console.log("c") }) //在鉤子上添加攔截器 h1.intercept({ //鉤子被call的時候觸發 call: (...args)=>{ console.log(...args, "-------------intercept call"); }, //定義攔截器的時候注冊taps register:(tap)=>{ console.log(tap, "------------------intercept register"); }, //循環方法 loop:(...args)=>{ console.log(...args, "---------------intercept loop") }, //tap調用前觸發 tap:(tap)=>{ console.log(tap, "---------------intercept tap") } }) //觸發鉤子 h1.call(6)2. 調試方法
最直接的方式是在 chrome 中通過斷點在關鍵代碼上進行調試,在如何使用 Chrome 調試webpack源碼中學到了調試的技巧:
3. 源碼分析我們可以用 node-inspector 在chrome中調試nodejs代碼,這比命令行中調試方便太多了。nodejs 從 v6.x 開始已經內置了一個 inspector,當我們啟動的時候可以加上 --inspect 參數即可:
node --inspect app.js然后打開chrome,打開一個新頁面,地址是: chrome://inspect,就可以在 chrome 中調試你的代碼了。
如果你的JS代碼是執行一遍就結束了,可能沒時間加斷點,那么你可能希望在啟動的時候自動在第一行自動加上斷點,可以使用這個參數 --inspect-brk,這樣會自動斷點在你的第一行代碼上。
安裝好Tapable包,根據上述方法,我們運行如下命令:
node --inspect-brk main.js
在構造函數處打上斷點,step into可以看到SyncHook繼承自Hook,上面定義了一個compile函數。
class SyncHook extends Hook { tapAsync() { throw new Error("tapAsync is not supported on a SyncHook"); } tapPromise() { throw new Error("tapPromise is not supported on a SyncHook"); } compile(options) { factory.setup(this, options); return factory.create(options); } }
再step into來到Hook.js
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; } ... }
h1初始化完成:
h1:{ call: ? lazyCompileHook(...args) callAsync: ? lazyCompileHook(...args) interceptors: [] promise: ? lazyCompileHook(...args) taps: [] _args: ["options"] _x: undefined }
Tapable采用觀察者模式來進行流程管理,在鉤子上使用tap方法注冊觀察者,鉤子被call時,觀察者對象上定義的回調函數按照不同規則觸發(鉤子類型不同,觸發順序不同)。
Step into tap方法:
//options="A", fn=f(arg) 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 ==>{type: "sync", fn: fn,name:options} options = Object.assign({ type: "sync", fn: fn }, options); if (typeof options.name !== "string" || options.name === "") throw new Error("Missing name for tap"); //這里調用攔截器上的register方法,當intercept定義在tap前時,會在這里調用intercept.register(options), 當intercept定義在tap后時,會在intercept方法中調用intercept.register(this.taps) options = this._runRegisterInterceptors(options); //根據before, stage 的值來排序this.taps = [{type: "sync", fn: fn,name:options}] this._insert(options); }
當三個觀察者注冊完成后,h1變為:
{ call: ? lazyCompileHook(...args) callAsync: ? lazyCompileHook(...args) interceptors: [] promise: ? lazyCompileHook(...args) taps:[ 0: {type: "sync", fn: ?, name: "A"} 1: {type: "sync", fn: ?, name: "B"} 2: {type: "sync", fn: ?, name: "C"} ] length: 3 __proto__: Array(0) _args: ["options"] _x: undefined }
在調用h1.intercept() 處step into,可以看到定義的攔截回調被推入this.interceptors中。
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]); } }
此時h1變為:
{ call: ? lazyCompileHook(...args) callAsync: ? lazyCompileHook(...args) interceptors: Array(1) 0: call: (...args) => {…} loop: (...args) => {…} register: (tap) => {…} tap: (tap) => {…} __proto__: Object length: 1 __proto__: Array(0) promise: ? lazyCompileHook(...args) taps: Array(3) 0: {type: "sync", fn: ?, name: "A"} 1: {type: "sync", fn: ?, name: "B"} 2: {type: "sync", fn: ?, name: "C"} length: 3 __proto__: Array(0) _args: ["options"] _x: undefined }
在觀察者和攔截器都注冊后,會保存在this.interceptors和this.taps中;當我們調用h1.call()函數后,會按照一定的順序調用它們,現在我們來看看具體的流程,在call方法調用時step into, 會來到Hook.js中的createCompileDelegate函數。
function createCompileDelegate(name, type) { return function lazyCompileHook(...args) { this[name] = this._createCall(type); return this[name](...args); }; }
因為_call函數定義在Hook原型上,并通過在構造函數中this.call=this.__call賦值。
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 } });
按照執行順序轉到 this._createCall:
_createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type }); }
在this.compile()處step into 跳轉到SyncHook.js上的compile方法上,其實我們在Hook.js上就可以看到,compile是需要在子類上重寫的方法, 在SyncHook上其實現如下:
compile(options) { factory.setup(this, options); return factory.create(options); } class SyncHookCodeFactory extends HookCodeFactory { content({ onError, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible }); } } const factory = new SyncHookCodeFactory();
在factory.setup處step into,可以看到factory.setup(this, options)其實只是把taps上注冊的回調推入this._x:
setup(instance, options) { instance._x = options.taps.map(t => t.fn); }
在factory.create中定義了this.interceptors和this.taps的具體執行順序,在這里step into:
//HookFactory.js create(options) { this.init(options); let fn; switch (this.options.type) { case "sync": fn = new Function( this.args(), ""use strict"; " + this.header() + this.content({ onError: err => `throw ${err}; `, onResult: result => `return ${result}; `, resultReturns: true, onDone: () => "", rethrowIfPossible: true }) ); break; case "async": .... case "promise": .... } this.deinit(); return fn; }
可以看到這里是通過new Function構造函數傳入this.interceptors和this.taps動態進行字符串拼接生成函數體執行的。
在this.header()中打斷點:
header() { let code = ""; if (this.needContext()) { code += "var _context = {}; "; } else { code += "var _context; "; } code += "var _x = this._x; "; if (this.options.interceptors.length > 0) { code += "var _taps = this.taps; "; code += "var _interceptors = this.interceptors; "; } 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 })}); `; } } return code; }
生成的code如下,其執行了攔截器中定義的call回調:
"var _context; var _x = this._x; var _taps = this.taps; var _interceptors = this.interceptors; _interceptors[0].call(options);
在this.content()打斷點,可以看到this.content定義在HookCodeFactory中:
class SyncHookCodeFactory extends HookCodeFactory { content({ onError, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible }); } }
其返回了定義在子類中的callTapsSeries方法:
callTapsSeries({ onError, onResult, resultReturns, onDone, doneReturns, rethrowIfPossible }) { if (this.options.taps.length === 0) return onDone(); const firstAsync = this.options.taps.findIndex(t => t.type !== "sync"); const somethingReturns = resultReturns || doneReturns || false; let code = ""; let current = onDone; for (let j = this.options.taps.length - 1; j >= 0; j--) { const i = j; const unroll = current !== onDone && this.options.taps[i].type !== "sync"; if (unroll) { code += `function _next${i}() { `; code += current(); code += `} `; current = () => `${somethingReturns ? "return " : ""}_next${i}(); `; } const done = current; const doneBreak = skipDone => { if (skipDone) return ""; return onDone(); }; const content = this.callTap(i, { onError: error => onError(i, error, done, doneBreak), onResult: onResult && (result => { return onResult(i, result, done, doneBreak); }), onDone: !onResult && done, rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync) }); current = () => content; } code += current(); return code; }
具體的拼接步驟這里就不詳述了,感興趣可以自己debugger,嘿嘿。最后返回的code為:
var _tap0 = _taps[0]; _interceptors[0].tap(_tap0); var _fn0 = _x[0]; _fn0(options); var _tap1 = _taps[1]; _interceptors[0].tap(_tap1); var _fn1 = _x[1]; _fn1(options); var _tap2 = _taps[2]; _interceptors[0].tap(_tap2); var _fn2 = _x[2]; _fn2(options); var _tap3 = _taps[3]; _interceptors[0].tap(_tap3); var _fn3 = _x[3]; _fn3(options);
這里定義了taps和其相應的攔截器的執行順序。
4. webpack調試技巧當我們調試webpack源碼是,經常需要在鉤子被call的代碼處調試到具體插件的執行過程,可以參考上述過程進行調試,具體步驟為:
在call處step into
在return處step into
得到生成的動態函數
(function anonymous(options ) { "use strict"; var _context; var _x = this._x; var _taps = this.taps; var _interceptors = this.interceptors; _interceptors[0].call(options); var _tap0 = _taps[0]; _interceptors[0].tap(_tap0); var _fn0 = _x[0]; _fn0(options); var _tap1 = _taps[1]; _interceptors[0].tap(_tap1); var _fn1 = _x[1]; _fn1(options); var _tap2 = _taps[2]; _interceptors[0].tap(_tap2); var _fn2 = _x[2]; _fn2(options); var _tap3 = _taps[3]; _interceptors[0].tap(_tap3); var _fn3 = _x[3]; _fn3(options); })
在fn(options)處打step into
回到tap注冊的函數
h1.tap("A", function (arg) { console.log("A",arg); return "b"; })
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106805.html
摘要:它的行為和的方法相似,用來注冊一個處理函數監聽器,來在信號事件發生時做一些事情他最終還是調用進行存儲。而就全部取出來執行。總結上面這些知識是理解插件和運行原理的前置條件更多內容待下次分解參考源碼版本說明參考鏈接 引言 去年3月的時候當時寫了一篇webpack2-update之路,到今天webpack已經到了4.2,更新挺快的,功能也在不斷的完善,webpack4特性之一就是零配置, w...
摘要:打開是個構造函數,定義了一些靜態屬性和方法我們先看在插件下地址上面寫的解釋就跟沒寫一樣在文件下我們看到輸出的一些對象方法每一個對應一個模塊而在下引入的下面,我們先研究引入的對象的英文單詞解釋,除了最常用的點擊手勢之外,還有一個意思是水龍頭進 打開compile class Compiler extends Tapable { constructor(context) { ...
摘要:開始對進行遍歷,當遇到等一些調用表達式時,觸發事件的執行,收集依賴,并。 1、Tapable Tap 的英文單詞解釋,除了最常用的 點擊 手勢之外,還有一個意思是 水龍頭 —— 在 webpack 中指的是后一種; Webpack 可以認為是一種基于事件流的編程范例,內部的工作流程都是基于 插件 機制串接起來; 而將這些插件粘合起來的就是webpack自己寫的基礎類 Tapable 是...
摘要:流程劃分縱觀整個打包過程,可以流程劃分為四塊。核心類關系圖功能實現模塊通過將源碼解析為樹并拆分,以及直至基礎模塊。通過的依賴和切割文件構建出含有和包含關系的對象。通過模版完成主入口文件的寫入,模版完成切割文件的寫入。 前言 插件plugin,webpack重要的組成部分。它以事件流的方式讓用戶可以直接接觸到webpack的整個編譯過程。plugin在編譯的關鍵地方觸發對應的事件,極大的...
摘要:調用的目的是為了注冊你的邏輯指定一個綁定到自身的事件鉤子。這個對象在啟動時被一次性建立,并配置好所有可操作的設置,包括,和。對象代表了一次資源版本構建。一個對象表現了當前的模塊資源編譯生成資源變化的文件以及被跟蹤依賴的狀態信息。 引言 在上一篇文章Tapable中介紹了其概念和一些原理用法,和這次討論分析webpack plugin的關聯很大。下面從實現一個插件入手。 demo插件 f...
閱讀 2386·2021-09-22 16:01
閱讀 3153·2021-09-22 15:41
閱讀 1171·2021-08-30 09:48
閱讀 490·2019-08-30 15:52
閱讀 3324·2019-08-30 13:57
閱讀 1713·2019-08-30 13:55
閱讀 3649·2019-08-30 11:25
閱讀 757·2019-08-29 17:25