gulp原始碼解析(三)—— 任務管理

發表於2017-02-09

上篇文章我們分別對 gulp 的 .src 和 .dest 兩個主要介面做了分析,今天打算把剩下的面紗一起揭開 —— 解析 gulp.task 的原始碼,瞭解在 gulp4.0 中是如何管理、處理任務的。

在先前的版本,gulp 使用了 orchestrator 模組來指揮、排序任務,但到了 4.0 則替換為 undertaker 來做統一管理。先前的一些 task 寫法會有所改變:

更多變化點,可以參考官方 changelog,或者在後文我們也將透過原始碼來介紹各 task API 用法。

o_div

從 gulp 的入口檔案來看,任務相關的介面都是從 undertaker 繼承:

接著看 undertaker 的入口檔案,發現其程式碼粒化的很好,每個介面都是單獨一個模組:

o_div

我們先從建構函式入手,可以知道 undertaker 其實是作為事件觸發器(EventEmitter)的子類:

這意味著你可以在它的例項上做事件繫結(.on)和事件觸發(.emit)處理。

另外在建構函式中,定義了一個內部屬性 _registry 作為暫存器(註冊/暫存器模式的實現,提供統一介面來儲存和讀取 tasks)

暫存器預設為 undertaker-registry 模組的例項,我們後續可以通過其對應介面來儲存和獲取任務:

undertaker-registry 的原始碼也簡略易懂:

o_div

雖然 undertaker 預設使用了 undertaker-registry 模組來做暫存器,但也允許使用自定義的介面去實現:

此處的 this.registry 介面提供自 lib/registry 模組:

o_div

接著看剩餘的介面定義:

其中 registry 是直接引用的 lib/registry 模組介面,在前面已經介紹過了,我們分別看看剩餘的介面(它們均存放在 lib 資料夾下)

o_div

1. this.task

為最常用的 gulp.task 介面提供功能實現,但本模組的程式碼量很少:

其中第一段 if 程式碼塊是為了相容如下寫法:

第二段 if 是對傳入的 fn 做判斷,為空則直接返回 name(任務名稱)對應的 taskFunction。即使用者可以通過 gulp.task(taskname) 來獲取任務方法。

此處的 _getTask 介面不外乎是對 this._registry.get 的簡單封裝。

o_div

2. this._setTask

名稱加了下劃線的一般都表示該介面只在內部使用,API 中不會對外暴露。而該介面雖然可以直觀瞭解為儲存 task,但它其實做了更多事情:

這裡的 helpers/metadata 模組其實是借用了 WeakMap 的能力,來把一個外部無引用的 taskFunction 物件作為 map 的 key 進行儲存,儲存的 value 值是一個 metadata 物件。

metadata 物件是用於描述 task 的具體資訊,包括名稱(name)、原始方法(orig)、依賴的任務節點(tree.nodes)等,後續我們即可以通過 metadata.get(task) 來獲取指定 task 的相關資訊(特別是任務依賴關係)了。

o_div

3. this.parallel

並行任務介面,可以輸入一個或多個 task:

該介面會返回一個帶有依賴關係 metadata 的 parallelFunction 供外層 task 介面註冊任務:

這裡有兩個最重要的地方需要具體分析下:

我們先看下 createExtensions 介面:

故 extensions 變數獲得了這樣的一個物件:

如果我們能把它們跟每個任務的建立、執行、錯誤處理過程關聯起來,例如在任務執行之前就呼叫 extensions.after(curTaskStorage),那麼就可以把擴充套件物件 extensions 的屬性方法作為任務各生命週期環節對應的回撥了。

做這一步關聯處理的,是這一行程式碼:

其中“create”引用自 bach/lib/parallel 模組,除了將擴充套件物件和任務關聯之外,它還利用 async-done 模組將每個 taskFunction 非同步化,且安排它們並行執行:

o_div

首先介紹下 async-done 模組,它可以把一個普通函式(傳入的第一個引數)非同步化:

執行結果:

561179-20170205213805198-896340355

那麼很明顯,undertaker(或 bach) 最終是利用 async-done 來讓傳入 this.parallel 介面的任務能夠非同步去執行(互不影響、互不依賴)

561179-20170208202429791-1024456682

o_div

我們接著回過頭看下 bach/lib/parallel 裡最重要的部分:

nowAndLater 即 now-and-later 模組,其 .map 介面如下:

在這段程式碼的 map 方法中,通過 for 迴圈遍歷了每個傳入 parallel 介面的 taskFunction,然後使用 iterator(async-done)將 taskFunction 非同步化並執行(執行完畢會觸發 hadler),並將 extensions 的各方法和 task 的生命週期關聯起來(比如在任務開始時執行“start”事件、任務出錯時執行“error”事件)

o_div

這裡還需留意一個點。我們回頭看 async-done 的示例程式碼:

async-done 支援要非同步化的函式,通過執行傳入的回撥來通知 async-done 當前方法可以結束並執行回撥了:

所以問題來了 —— 每次定義任務時,都需要傳入這個回撥引數嗎?即使傳入了,要在哪裡呼叫呢?

其實大部分情況,都是無須傳入回撥引數的。因為我們們常規定義的 gulp 任務都是基於流,而在 async-done 中有對流(或者Promise物件等)的消耗做了監聽(消耗完畢時自動觸發回撥)

這也是為何我們在定義任務的時候,都會建議在 gulp.src 前面加上一個“return”的原因:

o_div

另外還有一個遺留問題 —— bach/parallel 模組中返回函式裡的“done”引數是做啥的呢:

我們先看 now-and-later.map 裡是怎麼處理 done 的:

可以看出這個 done 不外乎是所有傳入任務執行完畢以後會被呼叫的方法,那麼它自然可以適應下面的場景了:

即 taskC 裡的“done”將在定義 taskE 的時候,作為通知 async-done 自身已經執行完畢了的回撥方法。

o_div

4. this.series

序列任務介面,可以輸入一個或多個 task:

series 介面的實現和 parallel 介面的基本是一致的,不一樣的地方只是在執行順序上的調整。

在 parallel 的程式碼中,是使用了 now-and-later 的 map 介面來處理傳入的任務執行順序;而在 series 中,使用的則是 now-and-later 的 mapSeries 介面:

通過改動 next 的位置,可以很好地要求傳入的任務必須一個接一個去執行(後一個任務在前一個任務執行完畢的回撥裡才會開始執行)

o_div

5. this.lastRun

這是一個工具方法(有點雞肋),用來記錄和獲取針對某個方法的執行前/後時間(如“1426000001111”)

底層所使用的是 last-run 模組,程式碼太簡單,就不贅述了:

o_div

6. this.tree

這是看起來不起眼(我們常規不需要手動呼叫到),但是又非常重要的一個介面 —— 它可以獲取當前註冊過的所有的任務的 metadata:

執行結果:

561179-20170208235905885-1215567678

那麼通過這個介面,gulp-cli 就很容易知道我們都定義了哪些任務、任務對應的方法是什麼、任務之間的依賴關係是什麼(因為 metadata 裡的“nodes”屬性表示了關係鏈)。。。從而合理地為我們安排任務的執行順序。

其實現也的確很簡單,我們看下 lib/tree 的原始碼:

不外乎是遍歷暫存器裡的任務,然後取它們的 metadata 資料來返回,簡單粗暴~

自此我們便對 gulp 是如何組織任務執行的原理有了一番瞭解,不得不說其核心模組 undertaker 還是有些複雜(或者說有點繞)的。

本文的註釋和示例程式碼可以從我的倉庫上獲取,讀者可自行下載除錯。共勉~

相關文章