顯微鏡下的webpack4:靈魂tapable,終於搞懂鉤子系列!

小美娜娜發表於2018-11-12

簡介

大家在看webpack原始碼的時候,有沒有感覺像再看天書,似乎沒有辦法一個檔案比如webpack.js從頭看到尾。感覺webpack的跳躍性很強,完全不知道程式在執行的時候,發生了什麼。完全不清楚這個事件是什麼時候發生的,比如loader是什麼時候執行的,plugin又是什麼時候出現的。webpack的程式錯綜複雜,完全迷失在程式之中。這究竟是為什麼呢?其實很簡單!因為webpack的靈魂Tapable!這個機制使得webpack異常的靈活,它有一句經典的話——Everything is a plugin!。由此可見webpack是靠外掛堆積起來的。而實現這個外掛機制的就是Tabable!

Events

webpack的靈魂Tapable,有點類似於nodejs的Events,都是註冊一個事件,然後到了適當的時候觸發。這裡的事件觸發是這樣繫結觸發的,通過on方法,繫結一個事件,emit方法出發一個事件。Tapable的機制和這類似,也是tap註冊一個事件,然後call執行這個事件。

const EventEmitter = require('events');
const myEmitter = new EventEmitter();
//on的第一個引數是事件名,之後emit可以通過這個事件名,從而觸發這個方法。
//on的第二個引數是回掉函式,也就是此事件的執行方法
myEmitter.on('newListener', (param1,param2) => {
	console.log("newListener",param1,param2)
});
//emit的第一個引數是觸發的事件名
//emit的第二個以後的引數是回撥函式的引數。
myEmitter.emit('newListener',111,222);
複製程式碼

Tapable究竟為何物

如果我們把Tapable的例項物件比作一顆參天大樹,那麼的每一根樹枝就是一個掛載的hook(鉤子),也就是Tapable之中給每一個事件分門別類的機制,比如編譯(compile.js)這個物件中,又執行(run)的鉤子,有構建(make)的鉤子,這些鉤子就像樹枝一樣,組成了一棵樹的骨幹,然後每個樹枝上的樹葉就是每個鉤子上面掛載的函式方法。樹枝(鉤子)越多,樹葉(函式)越多,此樹越茂密(程式越複雜)。

當然這只是一個簡易的理解。實際上,webpack中不止有一棵樹,每棵樹之間還有錯綜複雜的關係。比如有些方法如compilation.js中的一些方法,就要等compile.js中的make這個鉤子執行之後才會執行。那麼我們就從瞭解Tapable中鉤子的用法,來理解webpack中tapable。

以工作日為例,瞭解Tapable的用法

即使webpack中的每顆tapable的樹之間有錯綜複雜的關係,整個程式都有一個邏輯線,也就是遊戲中的主線劇情,我們先構建我們工作日的主線劇情。

主線劇情

讓我們來回一下,我們的日常工作日,應該大多數分成3個階段,上班前,上班中和下班後,這3個時間段。這三個時間段,我用了3中鉤子型別,普通型,流水型和熔斷型。 按照文件他們的解釋是這樣的:

  • 普通型basic:這個比較好理解就是按照tap的註冊順序一個個向下執行。
  • 流水型water:這個相對於basic的區別就是,雖然也是按照tap的順序一個個向下執行,但是如果上一個tap有返回值,那麼下一個tap的傳入引數就是上一個tap的返回值。
  • 熔斷型bail:這個相對於water的區別就是,如果返回了null以外的值,就不繼續執行了。

是不是感覺一個事件的訂閱釋出怎麼可以分出這麼多型別?不要急,每個型別都有他的作用!

鉤子的語法一般都是new 鉤子型別Hook([引數名1,引數名2,引數名3]),這裡的陣列是隻是提示你傳入引數有幾個,給了名字只是為了可讀性,如果你想寫一個別人看不懂的可以這樣new SyncHook(["a","b","c"]),這裡要注意這個引數名的型別是字串。如果沒有提前準備號需要傳入的引數,後續掛函式的時候,就無法傳入引數了。這個設計應該是為了日後好打理,告訴其他開發者,我傳入的引數型別。

class MyDaily {
	constructor() {
		this.hooks = {
			beforeWork: new SyncHook(["getUp"]),
            atWork: new SyncWaterfallHook(["workTask"]),
			afterWork: new SyncBailHook(["activity"])
		};
	}
	tapTap(){
	    //此處是行為
	}
	run(){
		this.hooks.beforeWork.call()
		this.hooks.atWork.call()
		this.hooks.afterWork.call()
	}
}
複製程式碼

一天我們不可能什麼事都不做,所以給鉤子上加點事,tap事情。先來點必然發生的,正常的上班族,自由職業不在考慮範圍內。早上我們會做什麼呢?穿衣服出門是必備的,不穿衣服沒法出門,不出門沒法上班。到了工作崗位,來點工作任務吧,比如我們需要做個ppt,然後用這個ppt去開會。下班後,本來想回家的,結果佳人有約,果然不回家。

tapTap(){
    this.hooks.beforeWork.tap("putOnCloth",()=>{
        console.log("穿衣服!")
    })
    this.hooks.beforeWork.tap("getOut",()=>{
        console.log("出門!")
    })
    this.hooks.atWork.tap("makePPT",()=>{
        console.log("做PPT!")
        return "你的ppt"
    })
    this.hooks.atWork.tap("meeting",(work)=>{
        console.log("帶著你的"+work+"開會!")
    })
    this.hooks.afterWork.tap("haveADate",()=>{
        console.log("約會咯!")
        return "約會真開心~"
    })
    this.hooks.afterWork.tap("goHome",()=>{
        console.log("溜了溜了!")
    })
}
複製程式碼

從上述我們可以看到通過主演劇情瞭解到各種同步鉤子的用法,可能難以理解就是熔斷型的鉤子,這個鉤子的存在意義就是,可以中斷一系列的事情,比如有地方出錯了,或者不需要進行下一步的操作我們就可以及時結束。

那麼如果我們做的事情都是非同步的,每一個事件之間都有聯絡,那麼我們就不能用同步的方法了。這個時候我們可以將sync鉤子替換成async的鉤子。

async相對於sync多了一個callback的機制,就是這樣的:

this.hooks.beforeWork.tapAsync("putOnCloth",(params,callback)=>{
	console.log("穿衣服!")
	callback();//此處無callback,則getOut這個掛載的函式便不會執行
})
this.hooks.beforeWork.tapAsync("getOut",(params,callback)=>{
	console.log("出門!")
	callback()//此處無callback,則beforeWork這個鉤子的回撥函式不會執行
})
this.hooks.beforeWork.callAsync("working",err=>{
	console.log(err+" end!")//如果最後一個tap的函式沒有callback則不會執行
})
複製程式碼

這裡我們可以將callback當作next函式,也就是下一個tap的函式的意思。以及如果當前tap的函式報錯,則可以在callback中加入錯誤的原因,那麼接下來的函式便不會執行,也就是這樣callback("errorReason"),那麼就直接回撥用當前鉤子的callAsync繫結的函式。

this.hooks.beforeWork.tapAsync("putOnCloth",(params,callback)=>{
	console.log("穿衣服!")
	callback("error");此處加入了錯誤原因,那麼直接callAsync,拋棄了getOut
})
this.hooks.beforeWork.tapAsync("getOut",(params,callback)=>{//直接skip了
	console.log("出門!")
})
this.hooks.beforeWork.callAsync("working",err=>{
	console.log(err+" end!")//error end!直接打出錯誤原因。
})
複製程式碼

小tips

大家發現沒有,Async和sync的區別在於Async通過callback來和後續的函式溝通,sync則是通過return一個值來做交流。所以,Async自帶sync中bail型別的鉤子。我曾經做了一個無聊的統計,因為鉤子太多了,我寫了一個程式碼遍歷了webpack這個專案,得出了所有鉤子的使用情況,結果如下所示:

SyncHook 69
SyncBailHook 63
SyncWaterfallHook 36
SyncLoopHook 0
AsyncParallelHook 1
AsyncParallelBailHook 0
AsyncSeriesHook 14
AsyncSeriesBailHook 0
AsyncSeriesWaterfallHook 5
複製程式碼

但是我發現AsyncSeriesBailHook竟然是0的時候,我很震驚,現在知道原因了,因為從作用上來說他和非同步鉤子的共能本身就重疊了,所以同理AsyncParallelBailHook這個平行執行的bail型別的鉤子也是0 。bail在Async中功能重複,因次用的很少。

言歸正傳,既然AsyncSeriesHook的callback通過第一個err引數來判斷是否非同步成功,不成功則直接callAsync回撥。那麼water型別的該如何傳遞引數?我們都知道water和basic的區別就在於basic每個非同步tap之間並無引數傳遞,而water則是引數傳遞。很簡單,在err後面再加一個引數,作為下一個tap的傳入值。

this.hooks.atWork.tapAsync("makePPT",(work,callback)=>{
    console.log("做PPT!")
    callback("沒做完 ","你的ppt")//第一個引數是err,上交你的報錯,第二個引數是你自定義要下一個tap處理的引數。如果有err,則忽略此引數。
})
this.hooks.atWork.tapAsync("meeting",(work,callback)=>{//因為ppt沒做完,所以開不了會
	console.log("帶著"+work+"開會!")
	callback()
})
this.hooks.atWork.callAsync("working",err=>{//沒做完來這裡領罰了。
	console.log(err+" end!")
})
複製程式碼

支線劇情

我們的日常生活!才不會這麼單調,怎麼會一路順順利利地走下來呢?每天都有不同的精彩啊!小插曲肯定少不了。

那麼我們就要相辦法將支線劇情插入主線劇情之中了。這個時候一個MyDaily的類已經放不下我們的精彩生活了。

以下班之後的精彩為例,我們不一定會直接回家也有可能約會蹦迪什麼的。所以這裡我們new一個名為Activity的類。假設我們的夜生活有兩個活動,一個派對活動,一個回家。那麼派對活動肯定有個流程,我們就用熔斷型的,為什麼呢!不開心了接下來就都別執行了!回家吧!回家的這個活動就是簡單的鉤子。

class Activity {
	constructor() {
		this.hooks = {
			goParty:new SyncBailHook(),
			goHome:new SyncHook()
		};
	}
	prepare(){
		this.hooks.goParty.tap("happy",()=>{
			console.log("happy party")
		})
		this.hooks.goParty.tap("unhappy",()=>{
			console.log("unhappy")
			return "go home"
		})
		this.hooks.goParty.tap("play",()=>{
			console.log("continue playing")
		})
		this.hooks.goHome.tap("goHome",()=>{
			console.log("I'm going to sleep")
		})
	}
	start(){
		this.hooks.goParty.call()
		this.hooks.goHome.call()
	}
}
複製程式碼

然後我們要將這個單獨的類掛到MyDaily的下面,畢竟這也是日常的一部分雖然非正式關卡。我們可以在工作結束自開始準備晚上的活動,等到一下班就開始我們豐富的夜生活。這個時候我們可以在鉤子的回撥函式中觸發另一個類中的鉤子狀態,啟用或著執行。

class MyDaily {
	constructor() {
		this.hooks = {
			....
		};
		this.activity=new Activity()//例項化Activity
	}
	run(){
	    ...
		this.hooks.atWork.callAsync("working",res=>{
			this.activity.prepare()//下班了大家可以躁動起來了
		})
		this.hooks.afterWork.callAsync("activity",err=>{
			this.activity.start()//去浪咯!
		})
	}
}
複製程式碼

總結

我在這裡只是舉了一個小例子,帶大家理解tapable是什麼。因為理解了tapable的特性,我們才能在之後有辦法理解webpack的機制,因為這種鉤子套鉤子的原因,我們很難看懂webpack的原始碼。下一篇文章我會帶大家看懂webpack的主線劇情和主要支線劇情(loader&plugin)的流程!

相關文章