摘要:源碼分析安裝好包,根據(jù)上述方法,我們運(yùn)行如下命令初始化在構(gòu)造函數(shù)處打上斷點(diǎn),可以看到繼承自,上面定義了一個(gè)函數(shù)。因?yàn)楹瘮?shù)定義在原型上,并通過在構(gòu)造函數(shù)中賦值。
Webpack源碼閱讀之Tapable
webpack采用Tapable來進(jìn)行流程控制,在這套體系上,內(nèi)部近百個(gè)插件有條不紊,還能支持外部開發(fā)自定義插件來擴(kuò)展功能,所以在閱讀webpack源碼前先了解Tapable的機(jī)制是很有必要的。
Tapable的基本使用方法就不介紹了,可以參考官方文檔
https://github.com/webpack/ta...
1. 例子從網(wǎng)上拷貝了一個(gè)簡單的使用例子:
//main.js const { SyncHook } = require("tapable") //創(chuàng)建一個(gè)簡單的同步串行鉤子 let h1 = new SyncHook(["arg1,arg2"]); //在鉤子上添加訂閱者,鉤子被call時(shí)會(huì)觸發(fā)訂閱的回調(diào)函數(shù) 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的時(shí)候觸發(fā) call: (...args)=>{ console.log(...args, "-------------intercept call"); }, //定義攔截器的時(shí)候注冊(cè)taps register:(tap)=>{ console.log(tap, "------------------intercept register"); }, //循環(huán)方法 loop:(...args)=>{ console.log(...args, "---------------intercept loop") }, //tap調(diào)用前觸發(fā) tap:(tap)=>{ console.log(tap, "---------------intercept tap") } }) //觸發(fā)鉤子 h1.call(6)2. 調(diào)試方法
最直接的方式是在 chrome 中通過斷點(diǎn)在關(guān)鍵代碼上進(jìn)行調(diào)試,在如何使用 Chrome 調(diào)試webpack源碼中學(xué)到了調(diào)試的技巧:
3. 源碼分析我們可以用 node-inspector 在chrome中調(diào)試nodejs代碼,這比命令行中調(diào)試方便太多了。nodejs 從 v6.x 開始已經(jīng)內(nèi)置了一個(gè) inspector,當(dāng)我們啟動(dòng)的時(shí)候可以加上 --inspect 參數(shù)即可:
node --inspect app.js然后打開chrome,打開一個(gè)新頁面,地址是: chrome://inspect,就可以在 chrome 中調(diào)試你的代碼了。
如果你的JS代碼是執(zhí)行一遍就結(jié)束了,可能沒時(shí)間加斷點(diǎn),那么你可能希望在啟動(dòng)的時(shí)候自動(dòng)在第一行自動(dòng)加上斷點(diǎn),可以使用這個(gè)參數(shù) --inspect-brk,這樣會(huì)自動(dòng)斷點(diǎn)在你的第一行代碼上。
安裝好Tapable包,根據(jù)上述方法,我們運(yùn)行如下命令:
node --inspect-brk main.js
在構(gòu)造函數(shù)處打上斷點(diǎn),step into可以看到SyncHook繼承自Hook,上面定義了一個(gè)compile函數(shù)。
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; //訂閱者數(shù)組 this.taps = []; //攔截器數(shù)組 this.interceptors = []; //原型上觸發(fā)鉤子的方法,為什么復(fù)制到構(gòu)造函數(shù)上? this.call = this._call; this.promise = this._promise; this.callAsync = this._callAsync; //用于保存訂閱者回調(diào)函數(shù)數(shù)組 this._x = undefined; } ... }
h1初始化完成:
h1:{ call: ? lazyCompileHook(...args) callAsync: ? lazyCompileHook(...args) interceptors: [] promise: ? lazyCompileHook(...args) taps: [] _args: ["options"] _x: undefined }
Tapable采用觀察者模式來進(jìn)行流程管理,在鉤子上使用tap方法注冊(cè)觀察者,鉤子被call時(shí),觀察者對(duì)象上定義的回調(diào)函數(shù)按照不同規(guī)則觸發(fā)(鉤子類型不同,觸發(fā)順序不同)。
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"); //這里調(diào)用攔截器上的register方法,當(dāng)intercept定義在tap前時(shí),會(huì)在這里調(diào)用intercept.register(options), 當(dāng)intercept定義在tap后時(shí),會(huì)在intercept方法中調(diào)用intercept.register(this.taps) options = this._runRegisterInterceptors(options); //根據(jù)before, stage 的值來排序this.taps = [{type: "sync", fn: fn,name:options}] this._insert(options); }
當(dāng)三個(gè)觀察者注冊(cè)完成后,h1變?yōu)椋?/p>
{ 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 }
在調(diào)用h1.intercept() 處step into,可以看到定義的攔截回調(diào)被推入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]); } }
此時(shí)h1變?yōu)椋?/p>
{ 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 }
在觀察者和攔截器都注冊(cè)后,會(huì)保存在this.interceptors和this.taps中;當(dāng)我們調(diào)用h1.call()函數(shù)后,會(huì)按照一定的順序調(diào)用它們,現(xiàn)在我們來看看具體的流程,在call方法調(diào)用時(shí)step into, 會(huì)來到Hook.js中的createCompileDelegate函數(shù)。
function createCompileDelegate(name, type) { return function lazyCompileHook(...args) { this[name] = this._createCall(type); return this[name](...args); }; }
因?yàn)開call函數(shù)定義在Hook原型上,并通過在構(gòu)造函數(shù)中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 } });
按照?qǐng)?zhí)行順序轉(zhuǎn)到 this._createCall:
_createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type }); }
在this.compile()處step into 跳轉(zhuǎn)到SyncHook.js上的compile方法上,其實(shí)我們?cè)贖ook.js上就可以看到,compile是需要在子類上重寫的方法, 在SyncHook上其實(shí)現(xiàn)如下:
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)其實(shí)只是把taps上注冊(cè)的回調(diào)推入this._x:
setup(instance, options) { instance._x = options.taps.map(t => t.fn); }
在factory.create中定義了this.interceptors和this.taps的具體執(zhí)行順序,在這里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構(gòu)造函數(shù)傳入this.interceptors和this.taps動(dòng)態(tài)進(jìn)行字符串拼接生成函數(shù)體執(zhí)行的。
在this.header()中打斷點(diǎn):
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如下,其執(zhí)行了攔截器中定義的call回調(diào):
"var _context; var _x = this._x; var _taps = this.taps; var _interceptors = this.interceptors; _interceptors[0].call(options);
在this.content()打斷點(diǎn),可以看到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和其相應(yīng)的攔截器的執(zhí)行順序。
4. webpack調(diào)試技巧當(dāng)我們調(diào)試webpack源碼是,經(jīng)常需要在鉤子被call的代碼處調(diào)試到具體插件的執(zhí)行過程,可以參考上述過程進(jìn)行調(diào)試,具體步驟為:
在call處step into
在return處step into
得到生成的動(dòng)態(tài)函數(shù)
(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注冊(cè)的函數(shù)
h1.tap("A", function (arg) { console.log("A",arg); return "b"; })
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/106805.html
摘要:它的行為和的方法相似,用來注冊(cè)一個(gè)處理函數(shù)監(jiān)聽器,來在信號(hào)事件發(fā)生時(shí)做一些事情他最終還是調(diào)用進(jìn)行存儲(chǔ)。而就全部取出來執(zhí)行。總結(jié)上面這些知識(shí)是理解插件和運(yùn)行原理的前置條件更多內(nèi)容待下次分解參考源碼版本說明參考鏈接 引言 去年3月的時(shí)候當(dāng)時(shí)寫了一篇webpack2-update之路,到今天webpack已經(jīng)到了4.2,更新挺快的,功能也在不斷的完善,webpack4特性之一就是零配置, w...
摘要:打開是個(gè)構(gòu)造函數(shù),定義了一些靜態(tài)屬性和方法我們先看在插件下地址上面寫的解釋就跟沒寫一樣在文件下我們看到輸出的一些對(duì)象方法每一個(gè)對(duì)應(yīng)一個(gè)模塊而在下引入的下面,我們先研究引入的對(duì)象的英文單詞解釋,除了最常用的點(diǎn)擊手勢之外,還有一個(gè)意思是水龍頭進(jìn) 打開compile class Compiler extends Tapable { constructor(context) { ...
摘要:開始對(duì)進(jìn)行遍歷,當(dāng)遇到等一些調(diào)用表達(dá)式時(shí),觸發(fā)事件的執(zhí)行,收集依賴,并。 1、Tapable Tap 的英文單詞解釋,除了最常用的 點(diǎn)擊 手勢之外,還有一個(gè)意思是 水龍頭 —— 在 webpack 中指的是后一種; Webpack 可以認(rèn)為是一種基于事件流的編程范例,內(nèi)部的工作流程都是基于 插件 機(jī)制串接起來; 而將這些插件粘合起來的就是webpack自己寫的基礎(chǔ)類 Tapable 是...
摘要:流程劃分縱觀整個(gè)打包過程,可以流程劃分為四塊。核心類關(guān)系圖功能實(shí)現(xiàn)模塊通過將源碼解析為樹并拆分,以及直至基礎(chǔ)模塊。通過的依賴和切割文件構(gòu)建出含有和包含關(guān)系的對(duì)象。通過模版完成主入口文件的寫入,模版完成切割文件的寫入。 前言 插件plugin,webpack重要的組成部分。它以事件流的方式讓用戶可以直接接觸到webpack的整個(gè)編譯過程。plugin在編譯的關(guān)鍵地方觸發(fā)對(duì)應(yīng)的事件,極大的...
摘要:調(diào)用的目的是為了注冊(cè)你的邏輯指定一個(gè)綁定到自身的事件鉤子。這個(gè)對(duì)象在啟動(dòng)時(shí)被一次性建立,并配置好所有可操作的設(shè)置,包括,和。對(duì)象代表了一次資源版本構(gòu)建。一個(gè)對(duì)象表現(xiàn)了當(dāng)前的模塊資源編譯生成資源變化的文件以及被跟蹤依賴的狀態(tài)信息。 引言 在上一篇文章Tapable中介紹了其概念和一些原理用法,和這次討論分析webpack plugin的關(guān)聯(lián)很大。下面從實(shí)現(xiàn)一個(gè)插件入手。 demo插件 f...
閱讀 2398·2021-09-22 16:01
閱讀 3164·2021-09-22 15:41
閱讀 1182·2021-08-30 09:48
閱讀 497·2019-08-30 15:52
閱讀 3335·2019-08-30 13:57
閱讀 1720·2019-08-30 13:55
閱讀 3671·2019-08-30 11:25
閱讀 767·2019-08-29 17:25