上篇文章咱們分別對 gulp 的 .src 和 .dest 兩個主要接口作了分析,今天打算把剩下的面紗一塊兒揭開 —— 解析 gulp.task 的源碼,瞭解在 gulp4.0 中是如何管理、處理任務的。node
在先前的版本,gulp 使用了 orchestrator 模塊來指揮、排序任務,但到了 4.0 則替換爲 undertaker 來作統一管理。先前的一些 task 寫法會有所改變:git
///////舊版寫法 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 用法。es6
從 gulp 的入口文件來看,任務相關的接口都是從 undertaker 繼承:github
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 的入口文件,發現其代碼粒化的很好,每一個接口都是單獨一個模塊:gulp
'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)的子類:數組
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):app
this._registry = new DefaultRegistry(); //undertaker-registry模塊 if (customRegistry) { //支持自定義寄存器 this.registry(customRegistry); }
寄存器默認爲 undertaker-registry 模塊的實例,咱們後續能夠經過其對應接口來存儲和獲取任務:dom
// 存儲任務(名稱+任務方法) this._registry.set(taskName, taskFunction); // 經過任務名稱獲取對應任務方法 this._registry.get(taskName); // 獲取存儲的所有任務 this._registry.task(); // { taskA : function(){...}, taskB : function(){...} }
undertaker-registry 的源碼也簡略易懂:異步
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 模塊來作寄存器,但也容許使用自定義的接口去實現:
function Undertaker(customRegistry) { //支持傳入自定義寄存器接口 EventEmitter.call(this); this._registry = new DefaultRegistry(); if (customRegistry) { //支持自定義寄存器 this.registry(customRegistry); } }
此處的 this.registry 接口提供自 lib/registry 模塊:
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;
接着看剩餘的接口定義:
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 接口提供功能實現,但本模塊的代碼量不多:
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 代碼塊是爲了兼容以下寫法:
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,但它其實作了更多事情:
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:
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 接口註冊任務:
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;
這裏有兩個最重要的地方須要具體分析下:
//新增一個擴展對象,用於後續給 taskFunction 加上生命週期回調 var extensions = createExtensions(this); //將 taskFunctions 裏的每個 taskFunction 加上生命週期回調,且異步化taskFunction,安排它們併發執行(調用fn的時候) var fn = create(args, extensions);
咱們先看下 createExtensions 接口:
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 變量得到了這樣的一個對象:
{ 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 的屬性方法做爲任務各生命週期環節對應的回調了。
作這一步關聯處理的,是這一行代碼:
var fn = create(args, extensions);
其中「create」引用自 bach/lib/parallel 模塊,除了將擴展對象和任務關聯以外,它還利用 async-done 模塊將每一個 taskFunction 異步化,且安排它們並行執行:
'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 模塊,它能夠把一個普通函數(傳入的第一個參數)異步化:
//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 裏最重要的部分:
function buildParallel() { //略 function parallel(done) { //遍歷tasks數組,將其生命週期和extensions屬性關聯起來,且將每一個task異步化,且併發執行 nowAndLater.map(args, asyncDone, extensions, done); } return parallel; } module.exports = buildParallel;
nowAndLater 即 now-and-later 模塊,其 .map 接口以下:
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 的示例代碼:
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 當前方法能夠結束並執行回調了:
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對象等)的消耗作了監聽(消耗完畢時自動觸發回調):
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」的緣由:
gulp.task('uglify', function(){ return gulp.src(['src/*.js']) //留意這裏的return .pipe(uglify()) .pipe(gulp.dest('dist')); });
另外還有一個遺留問題 —— bach/parallel 模塊中返回函數裏的「done」參數是作啥的呢:
function parallel(done) { //留意這裏的 done 參數 nowAndLater.map(args, asyncDone, extensions, done); }
咱們先看 now-and-later.map 裏是怎麼處理 done 的:
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 不外乎是全部傳入任務執行完畢之後會被調用的方法,那麼它天然能夠適應下面的場景了:
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:
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 接口:
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」):
var lastRun = require('last-run'); function myFunc(){} myFunc(); // 記錄函數執行的時間點(固然你也能夠放到「myFunc();」前面去) lastRun.capture(myFunc); // 獲取記錄的時間點 lastRun(myFunc);
底層所使用的是 last-run 模塊,代碼太簡單,就不贅述了:
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:
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 的源碼:
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 仍是有些複雜(或者說有點繞)的。
本文的註釋和示例代碼能夠從個人倉庫上獲取,讀者可自行下載調試。共勉~