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 还是有些复杂(或者说有点绕)的。

本文的注释和示例代码可以从我的仓库上获取,读者可自行下载调试。共勉~

posted @ 2017-02-09 00:13  vajoy  阅读(3530)  评论(1编辑  收藏  举报
Copyright © 2014 - 2022 VaJoy Studio