Gulp.task() 原始碼簡析

weixin_34146805發表於2018-08-10

前段時間一直在用 Webpack + Vue 開發 Web 應用,雖然使用了腳手架,但是 Webpack 繁瑣的配置一直讓我頭疼。直到有個前端朋友推薦我去學習下 Gulp,我屁顛屁顛地去了解下。

簡介

Gulp 是一款前端構建工具,無需寫一大堆繁雜的配置引數,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 執行順序,那麼呼叫 gulp.task() 時 gulp 都幹了些什麼;
  • 怎麼實現任務間的依賴以及任務的同步、非同步處理

版本

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 函式是 add 函式的別名,然而在當前模組 Gulp 原型中並沒有找到 add 函式的定義,很可能是繼承 Orchestrator 原型中的定義,所有 Orchestrator 是 Gulp 的核心模組。

詳析 Orchestrator

Git:https://github.com/robrich/orchestrator

A module for sequencing and executing tasks and dependencies in maximum concurrency
翻譯:在 最大併發性 中排序和執行任務及依賴關係的模組

檢視 ./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 具有事件監聽和事件觸發的功能。

Orchestrator 上定義了 4 個重要的屬性:
  1. doneCallback:回撥函式,當所有的任務完成是被呼叫
  2. seq:執行鏈(以最大併發能力執行的關鍵)
  3. tasks:使用者定義的所有任務配置資訊的集合
  4. isRunnning:標誌位,表示當前是不是正在執行任務
add() 函式定義
Orchestrator.prototype.add = function (name, dep, fn) {
    ... // 初始化值以及引數的校驗
    this.tasks[name] = {
        fn: fn,
        dep: dep,
        name: name
    };
    return this;
};

屬性 tasks 類似 Map 儲存著每個任務的名稱、依賴以及執行函式等等。

開始執行任務

一般情況下在控制檯輸入 gulp [task] 開始執行任務,那麼入口函式在哪裡呢?
在原始碼中不難發現 Orchestrator.prototype.start = function() { ... },看函式名就知道是啟動函式,這可以驗證

驗證入口函式

在 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 檔案,繼續 gulp.js 部分原始碼:

var argv = require('minimist')(process.argv.slice(2));

var tasks = argv._; // 控制檯 gulp [task] 的 task 名稱陣列
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 方法
  });
}
啟動函式 start() 做了些啥
Orchestrator.prototype.start = function() {
    var args, arg, names = [], seq = [];
    args = Array.prototype.slice.call(arguments, 0);
    ... // 省略掉引數初始化以及校驗 
    if (this.isRunning) {
        // 如果當前任務正在執行,則只結束並重置使用者指定啟動的任務
        this._resetSpecificTasks(names);
    } else {
        // 如果當前沒有任務執行則重置所有的任務
        this._resetAllTasks();
    }
    if (this.isRunning) {
        // 如果您再次呼叫start(),而之前的執行仍在執行中
        // 將新任務預先新增到現有任務佇列中
        names = names.concat(this.seq);
    }
    ...
    seq = [];
    try {
        this.sequence(this.tasks, names, seq, []); // 計算好任務作業鏈,這是實現最大併發性的關鍵函式
    } catch (err) {
        ...
        return this;
    }
    this.seq = seq;
    this.emit('start', {message:'seq: '+this.seq.join(',')}); // 觸發 start 事件
    if (!this.isRunning) {
        this.isRunning = true;
    }
    this._runStep();
    return this;
};

變數 names 儲存使用者指定執行的 tasks 名稱和任務鏈中為還未執行的 tasks 名稱;
簡單點說步驟:

  1. 該函式先檢查是否正在執行 tasks,如果正在執行並且正在執行的 tasks 中有使用者指定執行的 tasks,則停止並重置這些 tasks,然後將之前未指定的任務鏈(佇列)重新加到新任務鏈中;如果沒有執行任務,則重置所有定義的 tasks 的狀態。
  2. 呼叫 sequence(),計算作業鏈,用於計算機按序執行任務(下面講到)
  3. 觸發 start 事件
  4. 呼叫 _runStep(),執行作業鏈中的任務
任務作業鏈是怎麼計算的

答案在 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;

該函式有去重,形參 results 是排序後的結果
舉個栗子:

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

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

準備執行任務鏈 _runStep()

這個函式簡單不貼原始碼,它做的事情:

  1. 遍歷 seq 任務鏈,依次獲取 task 配置資訊
  2. 依次校驗準備執行的 task 的狀態以及所有依賴 tasks 的狀態
  3. 依次呼叫 _runTask() 才準備執行任務
  4. 若全部 tasks 完成,呼叫 doneCallback() 回撥函式
準備執行單個任務 _runTask()

步驟:

  1. 觸發 task_start 事件
  2. 設定當前 task 執行標誌為 true
  3. 重點:呼叫 runTask(fn, finishCallback) 真正執行 task,其中引數 fn 就是定義 task 時傳入的任務函式,回撥函式 finishCallback 做了三件事:
    1. 設定 task 為已完成、未執行
    2. 如果 task 執行中未丟擲異常,觸發 task_stop 事件;丟擲異常,觸發 task_err 事件
    3. 如果 task 執行中丟擲異常,停止所有任務,觸發 err 事件
    4. 若前三步正常(未拋異常),呼叫 _runStep 方法準備執行任務鏈下個 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() 函式用來通知當前 task 執行結束。

  • 當 task 有非同步操作時,我們想等待非同步任務中的非同步操作完成後再執行後續的任務怎麼做麼?

    1. 在非同步操作完成後執行一個回撥函式來通知 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 表示未定義回撥函式第一個引數),主動呼叫 finish() 通過結束,並指定執行方法為 同步

    r = task(cb);
    if (task.length === 0) {
        finish(null, 'sync');
    }
    

總結

  • Gulp 繼承 Orchestrator 實現了依賴、排序執行任務
  • 模組 sequencify 使用遞迴演算法,其實現任務去重、依賴排序,最後生成作業鏈,它是使得 以最大併發性執行 成為可能,同時確保了依賴間的執行順序
  • 模組 runTask 的方法 runTask() 確保了 task 同步、非同步執行順序

相關文章