Gulp.task() 源碼簡析

前段時間一直在用 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 個重要的屬性:
  1. doneCallback:回調函數(shù),當所有的任務完成是被調用
  2. seq:執(zhí)行鏈(以最大并發(fā)能力執(zhí)行的關鍵)
  3. tasks:用戶定義的所有任務配置信息的集合
  4. 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.jsgulp.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 名稱;
簡單點說步驟:

  1. 該函數(shù)先檢查是否正在執(zhí)行 tasks,如果正在執(zhí)行并且正在執(zhí)行的 tasks 中有用戶指定執(zhí)行的 tasks,則停止并重置這些 tasks,然后將之前未指定的任務鏈(隊列)重新加到新任務鏈中;如果沒有執(zhí)行任務,則重置所有定義的 tasks 的狀態(tài)。
  2. 調用 sequence(),計算作業(yè)鏈,用于計算機按序執(zhí)行任務(下面講到)
  3. 觸發(fā) start 事件
  4. 調用 _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 是排序后的結果
舉個栗子:

  1. 任務 A 依賴任務 B、C(依賴任務有序)
  2. 任務 C 依賴任務 D
  3. 任務 E 依賴任務 F
  4. 控制臺輸入 gulp E A

計算后任務鏈順序:F -> E-> B -> D -> C -> A

準備執(zhí)行任務鏈 _runStep()

這個函數(shù)簡單不貼源碼,它做的事情:

  1. 遍歷 seq 任務鏈,依次獲取 task 配置信息
  2. 依次校驗準備執(zhí)行的 task 的狀態(tài)以及所有依賴 tasks 的狀態(tài)
  3. 依次調用 _runTask() 才準備執(zhí)行任務
  4. 若全部 tasks 完成,調用 doneCallback() 回調函數(shù)
準備執(zhí)行單個任務 _runTask()

步驟:

  1. 觸發(fā) task_start 事件
  2. 設置當前 task 執(zhí)行標志為 true
  3. 重點:調用 runTask(fn, finishCallback) 真正執(zhí)行 task,其中參數(shù) fn 就是定義 task 時傳入的任務函數(shù),回調函數(shù) finishCallback 做了三件事:
    1. 設置 task 為已完成、未執(zhí)行
    2. 如果 task 執(zhí)行中未拋出異常,觸發(fā) task_stop 事件;拋出異常,觸發(fā) task_err 事件
    3. 如果 task 執(zhí)行中拋出異常,停止所有任務,觸發(fā) err 事件
    4. 若前三步正常(未拋異常),調用 _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ù)的任務怎么做么?

    1. 在異步操作完成后執(zhí)行一個回調函數(shù)來通知 gulp 這個異步任務已經完成
    cb = function (err) {
        finish(err, 'callback');
    };
    r = task(cb);
    
    1. 定義任務時返回一個流對象
    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);
    }
    
    1. 返回一個promise對象
    r = task(cb);
    if (r && typeof r.then === 'function') {
        r.then(function () {
             finish(null, 'promise');
        }, function(err) {
             finish(err, 'promise');
        });
    }
    
  • 當 task 沒異步操作時(通過 task.length0 表示未定義回調函數(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í)行順序
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • gulpjs是一個前端構建工具,與gruntjs相比,gulpjs無需寫一大堆繁雜的配置參數(shù),API也非常簡單,學...
    依依玖玥閱讀 3,306評論 7 55
  • gulpjs是一個前端構建工具,與gruntjs相比,gulpjs無需寫一大堆繁雜的配置參數(shù),API也非常簡單,學...
    build1024閱讀 587評論 0 0
  • gulpjs是一個前端構建工具,與gruntjs相比,gulpjs無需寫一大堆繁雜的配置參數(shù),API也非常簡單,學...
    井皮皮閱讀 1,403評論 0 10
  • 原文鏈接:www.talkingcoder.com 什么是gulp Gulp是可以自動化執(zhí)行任務的工具。在平時開發(fā)...
    李2牛閱讀 1,037評論 0 3
  • gulpjs是一個前端構建工具,與gruntjs相比,gulpjs無需寫一大堆繁雜的配置參數(shù),API也非常簡單,學...
    小裁縫sun閱讀 1,032評論 0 3

友情鏈接更多精彩內容