gulp源碼解析(三)—— 任務管理


上篇文章我們分別對 gulp 的 .src 和 .dest 兩個主要接口做了分析,今天打算把剩下的面紗一起揭開 —— 解析 gulp.task 的源碼,了解在 gulp4.0 中是如何管理、處理任務的。

在先前的版本,gulp 使用了 orchestrator 模塊來指揮、排序任務,但到了 4.0 則替換為 undertaker 來做統一管理。先前的一些 task 寫法會有所改變:

///////舊版寫法
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 繼承:

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 的入口文件,發現其代碼粒化的很好,每個接口都是單獨一個模塊:

'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)

  this._registry = new DefaultRegistry();  //undertaker-registry模塊
  if (customRegistry) {   //支持自定義寄存器
    this.registry(customRegistry);
  }

寄存器默認為 undertaker-registry 模塊的實例,我們后續可以通過其對應接口來存儲和獲取任務:

// 存儲任務(名稱+任務方法)
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;
View Code

雖然 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);
}
View Code

這也是為何我們在定義任務的時候,都會建議在 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;
View Code

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 還是有些復雜(或者說有點繞)的。

本文的注釋和示例代碼可以從我的倉庫上獲取,讀者可自行下載調試。共勉~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM