上篇文章我們分別對 gulp 的 .src 和 .dest 兩個主要介面做了分析,今天打算把剩下的面紗一起揭開 —— 解析 gulp.task 的原始碼,瞭解在 gulp4.0 中是如何管理、處理任務的。
在先前的版本,gulp 使用了 orchestrator 模組來指揮、排序任務,但到了 4.0 則替換為 undertaker 來做統一管理。先前的一些 task 寫法會有所改變:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
///////舊版寫法 gulp.task('uglify', function(){ return gulp.src(['src/*.js']) .pipe(uglify()) .pipe(gulp.dest('dist')); }); gulp.task('default', ['uglify']); ///////新版寫法1 gulp.task('uglify', function(){ return gulp.src(['src/*.js']) .pipe(uglify()) .pipe(gulp.dest('dist')); }); gulp.task('default', gulp.parallel('uglify')); ///////新版寫法2 function uglify(){ return gulp.src(['src/*.js']) .pipe(uglify()) .pipe(gulp.dest('dist')); } gulp.task(uglify); gulp.task('default', gulp.parallel(uglify)); |
更多變化點,可以參考官方 changelog,或者在後文我們也將透過原始碼來介紹各 task API 用法。
從 gulp 的入口檔案來看,任務相關的介面都是從 undertaker 繼承:
1 2 3 4 5 6 7 8 9 10 11 |
var util = require('util'); var Undertaker = require('undertaker');function Gulp() { Undertaker.call(this); this.task = this.task.bind(this); this.series = this.series.bind(this); this.parallel = this.parallel.bind(this); this.registry = this.registry.bind(this); this.tree = this.tree.bind(this); this.lastRun = this.lastRun.bind(this); } util.inherits(Gulp, Undertaker); |
接著看 undertaker 的入口檔案,發現其程式碼粒化的很好,每個介面都是單獨一個模組:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
'use strict'; var inherits = require('util').inherits; var EventEmitter = require('events').EventEmitter; var DefaultRegistry = require('undertaker-registry'); var tree = require('./lib/tree'); var task = require('./lib/task'); var series = require('./lib/series'); var lastRun = require('./lib/last-run'); var parallel = require('./lib/parallel'); var registry = require('./lib/registry'); var _getTask = require('./lib/get-task'); var _setTask = require('./lib/set-task'); function Undertaker(customRegistry) { EventEmitter.call(this); this._registry = new DefaultRegistry(); if (customRegistry) { this.registry(customRegistry); } this._settle = (process.env.UNDERTAKER_SETTLE === 'true'); } inherits(Undertaker, EventEmitter); Undertaker.prototype.tree = tree; Undertaker.prototype.task = task; Undertaker.prototype.series = series; Undertaker.prototype.lastRun = lastRun; Undertaker.prototype.parallel = parallel; Undertaker.prototype.registry = registry; Undertaker.prototype._getTask = _getTask; Undertaker.prototype._setTask = _setTask; module.exports = Undertaker; |
我們先從建構函式入手,可以知道 undertaker 其實是作為事件觸發器(EventEmitter)的子類:
1 2 3 4 5 6 7 8 9 10 11 12 |
function Undertaker(customRegistry) { EventEmitter.call(this); //super() this._registry = new DefaultRegistry(); if (customRegistry) { this.registry(customRegistry); } this._settle = (process.env.UNDERTAKER_SETTLE === 'true'); } inherits(Undertaker, EventEmitter); //繼承 EventEmitter |
這意味著你可以在它的例項上做事件繫結(.on)和事件觸發(.emit)處理。
另外在建構函式中,定義了一個內部屬性 _registry 作為暫存器(註冊/暫存器模式的實現,提供統一介面來儲存和讀取 tasks):
1 2 3 4 |
this._registry = new DefaultRegistry(); //undertaker-registry模組 if (customRegistry) { //支援自定義暫存器 this.registry(customRegistry); } |
暫存器預設為 undertaker-registry 模組的例項,我們後續可以通過其對應介面來儲存和獲取任務:
1 2 3 4 5 6 |
// 儲存任務(名稱+任務方法) this._registry.set(taskName, taskFunction); // 通過任務名稱獲取對應任務方法 this._registry.get(taskName); // 獲取儲存的全部任務 this._registry.task(); // { taskA : function(){...}, taskB : function(){...} } |
undertaker-registry 的原始碼也簡略易懂:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
function DefaultRegistry() { //對外免 new 處理 if (this instanceof DefaultRegistry === false) { return new DefaultRegistry(); } //初始化任務物件,用於儲存任務 this._tasks = {}; } // 初始化方法(僅做佔位使用) DefaultRegistry.prototype.init = function init(taker) {}; //返回指定任務方法 DefaultRegistry.prototype.get = function get(name) { return this._tasks[name]; }; //儲存任務 DefaultRegistry.prototype.set = function set(name, fn) { return this._tasks[name] = fn; }; //獲取任務物件 DefaultRegistry.prototype.tasks = function tasks() { var self = this; //克隆 this._tasks 物件,避免外部修改會對其有影響 return Object.keys(this._tasks).reduce(function(tasks, name) { tasks[name] = self.get(name); return tasks; }, {}); }; module.exports = DefaultRegistry; |
雖然 undertaker 預設使用了 undertaker-registry 模組來做暫存器,但也允許使用自定義的介面去實現:
1 2 3 4 5 6 7 8 9 10 |
function Undertaker(customRegistry) { //支援傳入自定義暫存器介面 EventEmitter.call(this); this._registry = new DefaultRegistry(); if (customRegistry) { //支援自定義暫存器 this.registry(customRegistry); } } |
此處的 this.registry 介面提供自 lib/registry 模組:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function setTasks(inst, task, name) { inst.set(name, task); return inst; } function registry(newRegistry) { if (!newRegistry) { return this._registry; } //驗證是否有效,主要判斷是否帶有 .get/.set/.tasks/.init 介面,若不符合則丟擲錯誤 validateRegistry(newRegistry); var tasks = this._registry.tasks(); //將現有 tasks 拷貝到新的暫存器上 this._registry = reduce(tasks, setTasks, newRegistry); //呼叫初始化介面(無論是否需要,暫存器務必帶有一個init介面) this._registry.init(this); } module.exports = registry; |
接著看剩餘的介面定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Undertaker.prototype.tree = tree; Undertaker.prototype.task = task; Undertaker.prototype.series = series; Undertaker.prototype.lastRun = lastRun; Undertaker.prototype.parallel = parallel; Undertaker.prototype.registry = registry; Undertaker.prototype._getTask = _getTask; Undertaker.prototype._setTask = _setTask; |
其中 registry 是直接引用的 lib/registry 模組介面,在前面已經介紹過了,我們分別看看剩餘的介面(它們均存放在 lib 資料夾下)。
1. this.task
為最常用的 gulp.task 介面提供功能實現,但本模組的程式碼量很少:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function task(name, fn) { if (typeof name === 'function') { fn = name; name = fn.displayName || fn.name; } if (!fn) { return this._getTask(name); } //儲存task this._setTask(name, fn); } module.exports = task; |
其中第一段 if 程式碼塊是為了相容如下寫法:
1 2 3 4 5 6 7 |
function uglify(){ return gulp.src(['src/*.js']) .pipe(uglify()) .pipe(gulp.dest('dist')); } gulp.task(uglify); gulp.task('default', gulp.parallel(uglify)); |
第二段 if 是對傳入的 fn 做判斷,為空則直接返回 name(任務名稱)對應的 taskFunction。即使用者可以通過 gulp.task(taskname) 來獲取任務方法。
此處的 _getTask 介面不外乎是對 this._registry.get 的簡單封裝。
2. this._setTask
名稱加了下劃線的一般都表示該介面只在內部使用,API 中不會對外暴露。而該介面雖然可以直觀瞭解為儲存 task,但它其實做了更多事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
var assert = require('assert'); var metadata = require('./helpers/metadata'); function set(name, fn) { //引數型別判斷,不合法則報錯 assert(name, 'Task name must be specified'); assert(typeof name === 'string', 'Task name must be a string'); assert(typeof fn === 'function', 'Task function must be specified'); //weakmap 裡要求 key 物件不能被引用過,所以有必要給 fn 多加一層簡單包裝 function taskWrapper() { return fn.apply(this, arguments); } //解除包裝 function unwrap() { return fn; } taskWrapper.unwrap = unwrap; taskWrapper.displayName = name; // 依賴 parallel/series 的 taskFunction 會先被設定過 metadata,其 branch 屬性會指向 parallel/series tasks var meta = metadata.get(fn) || {}; var nodes = []; if (meta.branch) { nodes.push(meta.tree); } // this._registry.set 介面最後會返回 taskWrapper var task = this._registry.set(name, taskWrapper) || taskWrapper; //設定任務的 metadata metadata.set(task, { name: name, orig: fn, tree: { label: name, type: 'task', nodes: nodes } }); } module.exports = set; |
這裡的 helpers/metadata 模組其實是借用了 WeakMap 的能力,來把一個外部無引用的 taskFunction 物件作為 map 的 key 進行儲存,儲存的 value 值是一個 metadata 物件。
metadata 物件是用於描述 task 的具體資訊,包括名稱(name)、原始方法(orig)、依賴的任務節點(tree.nodes)等,後續我們即可以通過 metadata.get(task) 來獲取指定 task 的相關資訊(特別是任務依賴關係)了。
3. this.parallel
並行任務介面,可以輸入一個或多個 task:
1 2 3 4 5 6 7 8 9 10 11 |
var undertaker = require('undertaker'); ut = new undertaker(); ut.task('taskA', function(){/*略*/}); ut.task('taskB', function(){/*略*/}); ut.task('taskC', function(){/*略*/}); ut.task('taskD', function(){/*略*/}); // taskD 需要在 'taskA', 'taskB', 'taskC' 執行完畢後才開始執行, // 其中 'taskA', 'taskB', 'taskC' 的執行是非同步的 ut.task('taskD', ut.parallel('taskA', 'taskB', 'taskC')); |
該介面會返回一個帶有依賴關係 metadata 的 parallelFunction 供外層 task 介面註冊任務:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
var bach = require('bach'); var metadata = require('./helpers/metadata'); var buildTree = require('./helpers/buildTree'); var normalizeArgs = require('./helpers/normalizeArgs'); var createExtensions = require('./helpers/createExtensions'); //並行任務介面 function parallel() { var create = this._settle ? bach.settleParallel : bach.parallel; //通過引數獲取存在暫存器(registry)中的 taskFunctions(陣列形式) var args = normalizeArgs(this._registry, arguments); //新增一個擴充套件物件,用於後續給 taskFunction 加上生命週期 var extensions = createExtensions(this); //將 taskFunctions 裡的每一個 taskFunction 加上生命週期,且非同步化 var fn = create(args, extensions); fn.displayName = '<parallel>'; //設定初步 metadata,方便外層 this.task 介面獲取依賴關係 metadata.set(fn, { name: fn.displayName, branch: true, //表示當前 task 是被依賴的(parallel)任務 tree: { label: fn.displayName, type: 'function', branch: true, nodes: buildTree(args) //返回每個 task metadata.tree 的集合(陣列) } }); //返回 parallel taskFunction 供外層 this.task 介面註冊任務 return fn; } module.exports = parallel; |
這裡有兩個最重要的地方需要具體分析下:
1 2 3 4 |
//新增一個擴充套件物件,用於後續給 taskFunction 加上生命週期回撥 var extensions = createExtensions(this); //將 taskFunctions 裡的每一個 taskFunction 加上生命週期回撥,且非同步化taskFunction,安排它們併發執行(呼叫fn的時候) var fn = create(args, extensions); |
我們先看下 createExtensions 介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
var uid = 0; function Storage(fn) { var meta = metadata.get(fn); this.fn = meta.orig || fn; this.uid = uid++; this.name = meta.name; this.branch = meta.branch || false; this.captureTime = Date.now(); this.startHr = []; } Storage.prototype.capture = function() { //新建一個名為runtimes的WeakMap,執行 runtimes.set(fn, captureTime); captureLastRun(this.fn, this.captureTime); }; Storage.prototype.release = function() { //從WM中釋放,即執行 runtimes.delete(fn); releaseLastRun(this.fn); }; function createExtensions(ee) { return { create: function(fn) { //建立 //返回一個 Storage 例項 return new Storage(fn); }, before: function(storage) { //執行前 storage.startHr = process.hrtime(); //別忘了 undertaker 例項是一個 EventEmitter ee.emit('start', { uid: storage.uid, name: storage.name, branch: storage.branch, time: Date.now(), }); }, after: function(result, storage) { //執行後 if (result && result.state === 'error') { return this.error(result.value, storage); } storage.capture(); ee.emit('stop', { uid: storage.uid, name: storage.name, branch: storage.branch, duration: process.hrtime(storage.startHr), time: Date.now(), }); }, error: function(error, storage) { //出錯 if (Array.isArray(error)) { error = error[0]; } storage.release(); ee.emit('error', { uid: storage.uid, name: storage.name, branch: storage.branch, error: error, duration: process.hrtime(storage.startHr), time: Date.now(), }); }, }; } module.exports = createExtensions; |
故 extensions 變數獲得了這樣的一個物件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ create: function (fn) { //建立 return new Storage(fn); }, before: function (storage) { //執行前 storage.startHr = process.hrtime(); ee.emit('start', metadata); }, after: function (result, storage) { //執行後 if (result && result.state === 'error') { return this.error(result.value, storage); } storage.capture(); ee.emit('stop', metadata); }, error: function (error, storage) { //出錯 if (Array.isArray(error)) { error = error[0]; } storage.release(); ee.emit('error', metadata); } } |
如果我們能把它們跟每個任務的建立、執行、錯誤處理過程關聯起來,例如在任務執行之前就呼叫 extensions.after(curTaskStorage),那麼就可以把擴充套件物件 extensions 的屬性方法作為任務各生命週期環節對應的回撥了。
做這一步關聯處理的,是這一行程式碼:
1 |
var fn = create(args, extensions); |
其中“create”引用自 bach/lib/parallel 模組,除了將擴充套件物件和任務關聯之外,它還利用 async-done 模組將每個 taskFunction 非同步化,且安排它們並行執行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
'use strict'; //獲取陣列除最後一個元素之外的所有元素,這裡用來獲取第一個引數(tasks陣列) var initial = require('lodash.initial'); //獲取陣列的最後一個元素,這裡用來獲取最後一個引數(extension物件) var last = require('lodash.last'); //將引入的函式非同步化 var asyncDone = require('async-done'); var nowAndLater = require('now-and-later'); var helpers = require('./helpers'); function buildParallel() { var args = helpers.verifyArguments(arguments); //驗證傳入引數合法性 var extensions = helpers.getExtensions(last(args)); //extension物件 if (extensions) { args = initial(args); //tasks陣列 } function parallel(done) { //遍歷tasks陣列,將其生命週期和extensions屬性關聯起來,且將每個task非同步化,且併發執行 nowAndLater.map(args, asyncDone, extensions, done); } return parallel; } module.exports = buildParallel; |
首先介紹下 async-done 模組,它可以把一個普通函式(傳入的第一個引數)非同步化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//demo1 var ad = require('async-done'); ad(function(cb){ console.log('first task starts!'); cb(null, 'first task done!') }, function(err, data){ console.log(data) }); ad(function(cb){ console.log('second task starts!'); setTimeout( cb.bind(this, null, 'second task done!'), 1000 ) }, function(err, data){ console.log(data) }); ad(function(cb){ console.log('third task starts!'); cb(null, 'third task done!') }, function(err, data){ console.log(data) }); |
執行結果:
那麼很明顯,undertaker(或 bach) 最終是利用 async-done 來讓傳入 this.parallel 介面的任務能夠非同步去執行(互不影響、互不依賴):
我們接著回過頭看下 bach/lib/parallel 裡最重要的部分:
1 2 3 4 5 6 7 8 9 10 11 12 |
function buildParallel() { //略 function parallel(done) { //遍歷tasks陣列,將其生命週期和extensions屬性關聯起來,且將每個task非同步化,且併發執行 nowAndLater.map(args, asyncDone, extensions, done); } return parallel; } module.exports = buildParallel; |
nowAndLater 即 now-and-later 模組,其 .map 介面如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
var once = require('once'); var helpers = require('./helpers'); function map(values, iterator, extensions, done) { if (typeof extensions === 'function') { done = extensions; extensions = {}; } if (typeof done !== 'function') { done = helpers.noop; //沒有傳入done則賦予一個空函式 } //讓 done 函式只執行一次 done = once(done); var keys = Object.keys(values); var length = keys.length; var count = length; var idx = 0; // 初始化一個空的、和values等長的陣列 var results = helpers.initializeResults(values); /** * helpers.defaultExtensions(extensions) 返回如下物件: * { create: extensions.create || defaultExts.create, before: extensions.before || defaultExts.before, after: extensions.after || defaultExts.after, error: extensions.error || defaultExts.error, } */ var exts = helpers.defaultExtensions(extensions); for (idx = 0; idx < length; idx++) { var key = keys[idx]; next(key); } function next(key) { var value = values[key]; //建立一個 Storage 例項 var storage = exts.create(value, key) || {}; //觸發'start'事件 exts.before(storage); //利用 async-done 將 taskFunction 轉為非同步方法並執行 iterator(value, once(handler)); function handler(err, result) { if (err) { //觸發'error'事件 exts.error(err, storage); return done(err, results); } //觸發'stop'事件 exts.after(result, storage); results[key] = result; if (--count === 0) { done(err, results); } } } } module.exports = map; |
在這段程式碼的 map 方法中,通過 for 迴圈遍歷了每個傳入 parallel 介面的 taskFunction,然後使用 iterator(async-done)將 taskFunction 非同步化並執行(執行完畢會觸發 hadler),並將 extensions 的各方法和 task 的生命週期關聯起來(比如在任務開始時執行“start”事件、任務出錯時執行“error”事件)。
這裡還需留意一個點。我們回頭看 async-done 的示例程式碼:
1 2 3 4 5 6 |
ad(function(cb){ //留意這裡的cb console.log('first task starts!'); cb(null, 'first task done!') //執行cb表示當前方法已結束,可以執行回撥了 }, function(err, data){ console.log(data) }); |
async-done 支援要非同步化的函式,通過執行傳入的回撥來通知 async-done 當前方法可以結束並執行回撥了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
gulp.task('TaskAfter', function(){ //略 }); gulp.task('uglify', function(){ return gulp.src(['src/*.js']) .pipe(uglify()) .pipe(gulp.dest('dist')); }); gulp.task('doSth', function(cb){ setTimeout(() => { console.log('最快也得5秒左右才給執行任務TaskAfter'); cb(); //表示任務 doSth 執行完畢,任務 TaskAfter 可以不用等它了 }, 5000) }); gulp.task('TaskAfter', gulp.parallel('uglify', 'doSth')); |
所以問題來了 —— 每次定義任務時,都需要傳入這個回撥引數嗎?即使傳入了,要在哪裡呼叫呢?
其實大部分情況,都是無須傳入回撥引數的。因為我們們常規定義的 gulp 任務都是基於流,而在 async-done 中有對流(或者Promise物件等)的消耗做了監聽(消耗完畢時自動觸發回撥):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
function asyncDone(fn, cb) { cb = once(cb); var d = domain.create(); d.once('error', onError); var domainBoundFn = d.bind(fn); function done() { d.removeListener('error', onError); d.exit(); //執行 cb return cb.apply(null, arguments); } function onSuccess(result) { return done(null, result); } function onError(error) { return done(error); } function asyncRunner() { var result = domainBoundFn(done); function onNext(state) { onNext.state = state; } function onCompleted() { return onSuccess(onNext.state); } if (result && typeof result.on === 'function') { // result 為 Stream 時 d.add(result); //消耗完畢了自動觸發 done eos(exhaust(result), eosConfig, done); return; } if (result && typeof result.subscribe === 'function') { // result 為 RxJS observable 時的處理 result.subscribe(onNext, onError, onCompleted); return; } if (result && typeof result.then === 'function') { // result 為 Promise 物件時的處理 result.then(onSuccess, onError); return; } } tick(asyncRunner); } |
這也是為何我們在定義任務的時候,都會建議在 gulp.src 前面加上一個“return”的原因:
1 2 3 4 5 |
gulp.task('uglify', function(){ return gulp.src(['src/*.js']) //留意這裡的return .pipe(uglify()) .pipe(gulp.dest('dist')); }); |
另外還有一個遺留問題 —— bach/parallel 模組中返回函式裡的“done”引數是做啥的呢:
1 2 3 |
function parallel(done) { //留意這裡的 done 引數 nowAndLater.map(args, asyncDone, extensions, done); } |
我們先看 now-and-later.map 裡是怎麼處理 done 的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
iterator(value, once(handler)); function handler(err, result) { if (err) { //觸發'error'事件 exts.error(err, storage); return done(err, results); //有任務出錯,故所有任務應停止呼叫 } //觸發'stop'事件 exts.after(result, storage); results[key] = result; if (--count === 0) { done(err, results); //所有任務已經呼叫完畢 } } |
可以看出這個 done 不外乎是所有傳入任務執行完畢以後會被呼叫的方法,那麼它自然可以適應下面的場景了:
1 2 3 4 5 |
gulp.task('taskA', function(){/*略*/}); gulp.task('taskB', function(){/*略*/}); gulp.task('taskC', gulp.parallel('taskA', 'taskB')); gulp.task('taskD', function(){/*略*/}); gulp.task('taskE', gulp.parallel('taskC', 'taskD')); //留意'taskC'本身也是一個parallelTask |
即 taskC 裡的“done”將在定義 taskE 的時候,作為通知 async-done 自身已經執行完畢了的回撥方法。
4. this.series
序列任務介面,可以輸入一個或多個 task:
1 2 3 4 5 6 7 8 |
ut.task('taskA', function(){/*略*/}); ut.task('taskB', function(){/*略*/}); ut.task('taskC', function(){/*略*/}); ut.task('taskD', function(){/*略*/}); // taskD 需要在 'taskA', 'taskB', 'taskC' 執行完畢後才開始執行, // 其中 'taskA', 'taskB', 'taskC' 的執行必須是按順序一個接一個的 ut.task('taskD', ut.series('taskA', 'taskB', 'taskC')); |
series 介面的實現和 parallel 介面的基本是一致的,不一樣的地方只是在執行順序上的調整。
在 parallel 的程式碼中,是使用了 now-and-later 的 map 介面來處理傳入的任務執行順序;而在 series 中,使用的則是 now-and-later 的 mapSeries 介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
next(key); function next(key) { var value = values[key]; var storage = exts.create(value, key) || {}; exts.before(storage); iterator(value, once(handler)); function handler(err, result) { if (err) { exts.error(err, storage); return done(err, results); //有任務出錯,故所有任務應停止呼叫 } exts.after(result, storage); results[key] = result; if (++idx >= length) { done(err, results); //全部任務已經結束了 } else { next(keys[idx]); //next不在是放在外面的迴圈裡,而是在任務的回撥裡 } } } |
通過改動 next 的位置,可以很好地要求傳入的任務必須一個接一個去執行(後一個任務在前一個任務執行完畢的回撥裡才會開始執行)。
5. this.lastRun
這是一個工具方法(有點雞肋),用來記錄和獲取針對某個方法的執行前/後時間(如“1426000001111”):
1 2 3 4 5 6 7 8 9 10 |
var lastRun = require('last-run'); function myFunc(){} myFunc(); // 記錄函式執行的時間點(當然你也可以放到“myFunc();”前面去) lastRun.capture(myFunc); // 獲取記錄的時間點 lastRun(myFunc); |
底層所使用的是 last-run 模組,程式碼太簡單,就不贅述了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
var assert = require('assert'); var WM = require('es6-weak-map'); var hasNativeWeakMap = require('es6-weak-map/is-native-implemented'); var defaultResolution = require('default-resolution'); var runtimes = new WM(); function isFunction(fn) { return (typeof fn === 'function'); } function isExtensible(fn) { if (hasNativeWeakMap) { // 支援原生 weakmap 直接返回 return true; } //平臺不支援 weakmap 的話則要求 fn 是可擴充套件屬性的物件,以確保還是能支援 es6-weak-map return Object.isExtensible(fn); } //timeResolution引數用於決定返回的時間戳後幾位數字要置0 function lastRun(fn, timeResolution) { assert(isFunction(fn), 'Only functions can check lastRun'); assert(isExtensible(fn), 'Only extensible functions can check lastRun'); //先獲取捕獲時間 var time = runtimes.get(fn); if (time == null) { return; } //defaultResolution介面 - timeResolution格式處理(轉十進位制整數) var resolution = defaultResolution(timeResolution); //減去(time % resolution)的作用是將後n位置0 return time - (time % resolution); } function capture(fn, timestamp) { assert(isFunction(fn), 'Only functions can be captured'); assert(isExtensible(fn), 'Only extensible functions can be captured'); timestamp = timestamp || Date.now(); //(在任務執行的時候)儲存捕獲時間資訊 runtimes.set(fn, timestamp); } function release(fn) { assert(isFunction(fn), 'Only functions can be captured'); assert(isExtensible(fn), 'Only extensible functions can be captured'); runtimes.delete(fn); } //繫結靜態方法 lastRun.capture = capture; lastRun.release = release; module.exports = lastRun; |
6. this.tree
這是看起來不起眼(我們常規不需要手動呼叫到),但是又非常重要的一個介面 —— 它可以獲取當前註冊過的所有的任務的 metadata:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var undertaker = require('undertaker'); ut = new undertaker(); ut.task('taskA', function(cb){console.log('A'); cb()}); ut.task('taskB', function(cb){console.log('B'); cb()}); ut.task('taskC', function(cb){console.log('C'); cb()}); ut.task('taskD', function(cb){console.log('D'); cb()}); ut.task('taskE', function(cb){console.log('E'); cb()}); ut.task('taskC', ut.series('taskA', 'taskB')); ut.task('taskE', ut.parallel('taskC', 'taskD')); var tree = ut.tree(); console.log(tree); |
執行結果:
那麼通過這個介面,gulp-cli 就很容易知道我們都定義了哪些任務、任務對應的方法是什麼、任務之間的依賴關係是什麼(因為 metadata 裡的“nodes”屬性表示了關係鏈)。。。從而合理地為我們安排任務的執行順序。
其實現也的確很簡單,我們看下 lib/tree 的原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
var defaults = require('lodash.defaults'); var map = require('lodash.map'); var metadata = require('./helpers/metadata'); function tree(opts) { opts = defaults(opts || {}, { deep: false, }); var tasks = this._registry.tasks(); //獲取所有儲存的任務 var nodes = map(tasks, function(task) { //遍歷並返回metadata陣列 var meta = metadata.get(task); if (opts.deep) { //如果傳入了 {deep: true},則從 meta.tree 開始返回 return meta.tree; } return meta.tree.label; //從 meta.tree.label 開始返回 }); return { //返回Tasks物件 label: 'Tasks', nodes: nodes }; } module.exports = tree; |
不外乎是遍歷暫存器裡的任務,然後取它們的 metadata 資料來返回,簡單粗暴~
自此我們便對 gulp 是如何組織任務執行的原理有了一番瞭解,不得不說其核心模組 undertaker 還是有些複雜(或者說有點繞)的。
本文的註釋和示例程式碼可以從我的倉庫上獲取,讀者可自行下載除錯。共勉~