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

簡介

你們在看webpack源碼的時候,有沒有感受像再看天書,彷佛沒有辦法一個文件好比webpack.js從頭看到尾。感受webpack的跳躍性很強,徹底不知道程序在運行的時候,發生了什麼。徹底不清楚這個事件是何時發生的,好比loader是何時執行的,plugin又是何時出現的。webpack的程序錯綜複雜,徹底迷失在程序之中。這到底是爲何呢?其實很簡單!由於webpack的靈魂Tapable!這個機制使得webpack異常的靈活,它有一句經典的話——Everything is a plugin!。因而可知webpack是靠插件堆積起來的。而實現這個插件機制的就是Tabable!html

Events

webpack的靈魂Tapable,有點相似於nodejs的Events,都是註冊一個事件,而後到了適當的時候觸發。這裏的事件觸發是這樣綁定觸發的,經過on方法,綁定一個事件,emit方法出發一個事件。Tapable的機制和這相似,也是tap註冊一個事件,而後call執行這個事件。node

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

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

以工做日爲例,瞭解Tapable的用法

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

主線劇情

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

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

是否是感受一個事件的訂閱發佈怎麼能夠分出這麼多類型?不要急,每一個類型都有他的做用!bash

鉤子的語法通常都是new 鉤子類型Hook([參數名1,參數名2,參數名3]),這裏的數組是隻是提示你傳入參數有幾個,給了名字只是爲了可讀性,若是你想寫一個別人看不懂的能夠這樣new SyncHook(["a","b","c"]),這裏要注意這個參數名的類型是字符串。若是沒有提早準備號須要傳入的參數,後續掛函數的時候,就沒法傳入參數了。這個設計應該是爲了往後好打理,告訴其餘開發者,我傳入的參數類型。app

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("溜了溜了!")
    })
}
複製代碼

從上述咱們能夠看到經過主演劇情瞭解到各類同步鉤子的用法,可能難以理解就是熔斷型的鉤子,這個鉤子的存在乎義就是,能夠中斷一系列的事情,好比有地方出錯了,或者不須要進行下一步的操做咱們就能夠及時結束。async

那麼若是咱們作的事情都是異步的,每個事件之間都有聯繫,那麼咱們就不能用同步的方法了。這個時候咱們能夠將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)的流程!

相關文章
相關標籤/搜索