前段時間一直在用 Webpack + Vue 開發(fā) Web 應用,雖然使用了腳手架,但是 Webpack 繁瑣的配置一直讓我頭疼。直到有個前端朋友推薦我去學習下 Gulp,我屁顛屁顛地去了解下。
簡介
Gulp 是一款前端構建工具,無需寫一大堆繁雜的配置參數(shù),API也非常簡單,學習起來很容易,如果你沒接觸過該款工具,請您學習后再讀會比較容易。
舉個栗子:
var gulp = require('gulp')
gulp.task('one', function(cb) {
setTimeout(() => {
console.log('one is done')
cb()
}, 2000);
})
gulp.task('two', ['one'], function() {
console.log('two is done')
})
- gulp 能按依賴、同步、異步確保 task 執(zhí)行順序,那么調用
gulp.task()時 gulp 都干了些什么; - 怎么實現(xiàn)任務間的依賴以及任務的同步、異步處理
版本
Gulp v3.9.1
簡析 Gulp
查看 ./node_modules/gulp/index.js
var Orchestrator = require('orchestrator');
function Gulp() {
Orchestrator.call(this);
}
util.inherits(Gulp, Orchestrator);
var inst = new Gulp();
module.exports = inst;
很明顯 Gulp 是繼承 Orchestrator 的,并且 exports 是個實例對象,因此每當 require() 后變量是全局單例。其中有行代碼:
Gulp.prototype.task = Gulp.prototype.add;
Gulp 的 task 函數(shù)是 add 函數(shù)的別名,然而在當前模塊 Gulp 原型中并沒有找到 add 函數(shù)的定義,很可能是繼承 Orchestrator 原型中的定義,所有 Orchestrator 是 Gulp 的核心模塊。
詳析 Orchestrator
Git:https://github.com/robrich/orchestrator
A module for sequencing and executing tasks and dependencies in maximum concurrency
翻譯:在 最大并發(fā)性 中排序和執(zhí)行任務及依賴關系的模塊
查看 ./node_modules/orchestrator/index.js
var util = require('util');
var events = require('events');
var EventEmitter = events.EventEmitter;
var Orchestrator = function () {
EventEmitter.call(this);
this.doneCallback = undefined;
this.seq = [];
this.tasks = {};
this.isRunning = false;
};
util.inherits(Orchestrator, EventEmitter);
module.exports = Orchestrator;
很明顯 Orchestrator 是繼承 EventEmitter,所以 Gulp 具有事件監(jiān)聽和事件觸發(fā)的功能。
Orchestrator 上定義了 4 個重要的屬性:
- doneCallback:回調函數(shù),當所有的任務完成是被調用
- seq:執(zhí)行鏈(以最大并發(fā)能力執(zhí)行的關鍵)
- tasks:用戶定義的所有任務配置信息的集合
- isRunnning:標志位,表示當前是不是正在執(zhí)行任務
add() 函數(shù)定義
Orchestrator.prototype.add = function (name, dep, fn) {
... // 初始化值以及參數(shù)的校驗
this.tasks[name] = {
fn: fn,
dep: dep,
name: name
};
return this;
};
屬性 tasks 類似 Map 存儲著每個任務的名稱、依賴以及執(zhí)行函數(shù)等等。
開始執(zhí)行任務
一般情況下在控制臺輸入 gulp [task] 開始執(zhí)行任務,那么入口函數(shù)在哪里呢?
在源碼中不難發(fā)現(xiàn) Orchestrator.prototype.start = function() { ... },看函數(shù)名就知道是啟動函數(shù),這可以驗證
驗證入口函數(shù)
在 npm 本地倉庫目錄下 ./gulp.cmd 源碼:
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\node_modules\gulp\bin\gulp.js" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\node_modules\gulp\bin\gulp.js" %*
)
很明顯運行了 node ./node_modules/gulp/bin/gulp.js,gulp.js 是入口 Js 文件,繼續(xù) gulp.js 部分源碼:
var argv = require('minimist')(process.argv.slice(2));
var tasks = argv._; // 控制臺 gulp [task] 的 task 名稱數(shù)組
var toRun = tasks.length ? tasks : ['default']; // 若沒有指定 task,則按照默認值
var cli = new Liftoff({
name: 'gulp',
...
});
cli.launch({
cwd: argv.cwd,
...
}, handleArguments);
function handleArguments(env) {
...
var gulpInst = require(env.modulePath); // 關鍵點:導入模塊實例對象,也就是 gulp
...
process.nextTick(function() {
...
// 這里就調用了入口方法
gulpInst.start.apply(gulpInst, toRun); // 調用了 gulp 對象的 start 方法
});
}
啟動函數(shù) start() 做了些啥
Orchestrator.prototype.start = function() {
var args, arg, names = [], seq = [];
args = Array.prototype.slice.call(arguments, 0);
... // 省略掉參數(shù)初始化以及校驗
if (this.isRunning) {
// 如果當前任務正在執(zhí)行,則只結束并重置用戶指定啟動的任務
this._resetSpecificTasks(names);
} else {
// 如果當前沒有任務執(zhí)行則重置所有的任務
this._resetAllTasks();
}
if (this.isRunning) {
// 如果您再次調用start(),而之前的運行仍在運行中
// 將新任務預先添加到現(xiàn)有任務隊列中
names = names.concat(this.seq);
}
...
seq = [];
try {
this.sequence(this.tasks, names, seq, []); // 計算好任務作業(yè)鏈,這是實現(xiàn)最大并發(fā)性的關鍵函數(shù)
} catch (err) {
...
return this;
}
this.seq = seq;
this.emit('start', {message:'seq: '+this.seq.join(',')}); // 觸發(fā) start 事件
if (!this.isRunning) {
this.isRunning = true;
}
this._runStep();
return this;
};
變量 names 保存用戶指定執(zhí)行的 tasks 名稱和任務鏈中為還未執(zhí)行的 tasks 名稱;
簡單點說步驟:
- 該函數(shù)先檢查是否正在執(zhí)行 tasks,如果正在執(zhí)行并且正在執(zhí)行的 tasks 中有用戶指定執(zhí)行的 tasks,則停止并重置這些 tasks,然后將之前未指定的任務鏈(隊列)重新加到新任務鏈中;如果沒有執(zhí)行任務,則重置所有定義的 tasks 的狀態(tài)。
- 調用
sequence(),計算作業(yè)鏈,用于計算機按序執(zhí)行任務(下面講到) - 觸發(fā)
start事件 - 調用
_runStep(),執(zhí)行作業(yè)鏈中的任務
任務作業(yè)鏈是怎么計算的
答案在 sequencify 模塊中,使用了簡單的遞歸算法,見源碼:
var sequence = function (tasks, names, results, nest) {
var i, name, node, e, j;
nest = nest || [];
for (i = 0; i < names.length; i++) {
name = names[i];
// de-dup results
if (results.indexOf(name) === -1) {
node = tasks[name];
if (!node) {
e = new Error('task "'+name+'" is not defined');
e.missingTask = name;
e.taskList = [];
for (j in tasks) {
if (tasks.hasOwnProperty(j)) {
e.taskList.push(tasks[j].name);
}
}
throw e;
}
if (nest.indexOf(name) > -1) {
nest.push(name);
e = new Error('Recursive dependencies detected: '+nest.join(' -> '));
e.recursiveTasks = nest;
e.taskList = [];
for (j in tasks) {
if (tasks.hasOwnProperty(j)) {
e.taskList.push(tasks[j].name);
}
}
throw e;
}
if (node.dep.length) {
nest.push(name);
sequence(tasks, node.dep, results, nest); // recurse
nest.pop(name);
}
results.push(name);
}
}
};
module.exports = sequence;
該函數(shù)有去重,形參 results 是排序后的結果
舉個栗子:
- 任務 A 依賴任務 B、C(依賴任務有序)
- 任務 C 依賴任務 D
- 任務 E 依賴任務 F
- 控制臺輸入
gulp E A
計算后任務鏈順序:F -> E-> B -> D -> C -> A
準備執(zhí)行任務鏈 _runStep()
這個函數(shù)簡單不貼源碼,它做的事情:
- 遍歷
seq任務鏈,依次獲取 task 配置信息 - 依次校驗準備執(zhí)行的 task 的狀態(tài)以及所有依賴 tasks 的狀態(tài)
- 依次調用
_runTask()才準備執(zhí)行任務 - 若全部 tasks 完成,調用
doneCallback()回調函數(shù)
準備執(zhí)行單個任務 _runTask()
步驟:
- 觸發(fā)
task_start事件 - 設置當前 task 執(zhí)行標志為 true
- 重點:調用
runTask(fn, finishCallback)真正執(zhí)行 task,其中參數(shù)fn就是定義 task 時傳入的任務函數(shù),回調函數(shù)finishCallback做了三件事:- 設置 task 為已完成、未執(zhí)行
- 如果 task 執(zhí)行中未拋出異常,觸發(fā)
task_stop事件;拋出異常,觸發(fā)task_err事件 - 如果 task 執(zhí)行中拋出異常,停止所有任務,觸發(fā)
err事件 - 若前三步正常(未拋異常),調用
_runStep方法準備執(zhí)行任務鏈下個 task
怎么處理同步、異步任務
答案在 runTask() 方法中,源碼:
var eos = require('end-of-stream');
var consume = require('stream-consume');
module.exports = function (task, done) {
var that = this, finish, cb, isDone = false, start, r;
finish = function (err, runMethod) {
var hrDuration = process.hrtime(start);
if (isDone && !err) {
err = new Error('task completion callback called too many times');
}
isDone = true;
var duration = hrDuration[0] + (hrDuration[1] / 1e9); // seconds
done.call(that, err, {
duration: duration, // seconds
hrDuration: hrDuration, // [seconds,nanoseconds]
runMethod: runMethod
});
};
cb = function (err) {
finish(err, 'callback');
};
try {
start = process.hrtime();
r = task(cb);
} catch (err) {
return finish(err, 'catch');
}
if (r && typeof r.then === 'function') {
// wait for promise to resolve
// FRAGILE: ASSUME: Promises/A+, see http://promises-aplus.github.io/promises-spec/
r.then(function () {
finish(null, 'promise');
}, function(err) {
finish(err, 'promise');
});
} else if (r && typeof r.pipe === 'function') {
// wait for stream to end
eos(r, { error: true, readable: r.readable, writable: r.writable && !r.readable }, function(err){
finish(err, 'stream');
});
// Ensure that the stream completes
consume(r);
} else if (task.length === 0) {
// synchronous, function took in args.length parameters, and the callback was extra
finish(null, 'sync');
}
};
最主要的是 finish() 函數(shù)用來通知當前 task 執(zhí)行結束。
-
當 task 有異步操作時,我們想等待異步任務中的異步操作完成后再執(zhí)行后續(xù)的任務怎么做么?
- 在異步操作完成后執(zhí)行一個回調函數(shù)來通知 gulp 這個異步任務已經完成
cb = function (err) { finish(err, 'callback'); }; r = task(cb);- 定義任務時返回一個流對象
r = task(cb); if (r && typeof r.pipe === 'function') { eos(r, { error: true, readable: r.readable, writable: r.writable && !r.readable }, function(err){ finish(err, 'stream'); }); // Ensure that the stream completes consume(r); }- 返回一個promise對象
r = task(cb); if (r && typeof r.then === 'function') { r.then(function () { finish(null, 'promise'); }, function(err) { finish(err, 'promise'); }); } -
當 task 沒異步操作時(通過
task.length為0表示未定義回調函數(shù)第一個參數(shù)),主動調用finish()通過結束,并指定運行方法為 同步r = task(cb); if (task.length === 0) { finish(null, 'sync'); }
總結
- Gulp 繼承 Orchestrator 實現(xiàn)了依賴、排序執(zhí)行任務
- 模塊 sequencify 使用遞歸算法,其實現(xiàn)任務去重、依賴排序,最后生成作業(yè)鏈,它是使得 以最大并發(fā)性執(zhí)行 成為可能,同時確保了依賴間的執(zhí)行順序
- 模塊 runTask 的方法
runTask()確保了 task 同步、異步執(zhí)行順序