上篇文章我们分别对 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;

虽然 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);
}

这也是为何我们在定义任务的时候,都会建议在 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;

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

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

gulp源码解析(三)—— 任务管理的更多相关文章

  1. gulp源码解析(一)—— Stream详解

    作为前端,我们常常会和 Stream 有着频繁的接触.比如使用 gulp 对项目进行构建的时候,我们会使用 gulp.src 接口将匹配到的文件转为 stream(流)的形式,再通过 .pipe() ...

  2. Celery 源码解析三: Task 对象的实现

    Task 的实现在 Celery 中你会发现有两处,一处位于 celery/app/task.py,这是第一个:第二个位于 celery/task/base.py 中,这是第二个.他们之间是有关系的, ...

  3. Mybatis源码解析(三) —— Mapper代理类的生成

    Mybatis源码解析(三) -- Mapper代理类的生成   在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject( ...

  4. ReactiveCocoa源码解析(三) Signal代码的基本实现

    上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...

  5. ReactiveSwift源码解析(三) Signal代码的基本实现

    上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...

  6. React的React.createRef()/forwardRef()源码解析(三)

    1.refs三种使用用法 1.字符串 1.1 dom节点上使用 获取真实的dom节点 //使用步骤: 1. <input ref="stringRef" /> 2. t ...

  7. gulp源码解析(二)—— vinyl-fs

    在上一篇文章我们对 Stream 的特性及其接口进行了介绍,gulp 之所以在性能上好于 grunt,主要是因为有了 Stream 助力来做数据的传输和处理. 那么我们不难猜想出,在 gulp 的任务 ...

  8. Spring源码解析三:IOC容器的依赖注入

    一般情况下,依赖注入的过程是发生在用户第一次向容器索要Bean是触发的,而触发依赖注入的地方就是BeanFactory的getBean方法. 这里以DefaultListableBeanFactory ...

  9. jQuery 源码解析(三) pushStack方法 详解

    该函数用于创建一个新的jQuery对象,然后将一个DOM元素集合加入到jQuery栈中,最后返回该jQuery对象,有三个参数,如下: elems Array类型 将要压入 jQuery 栈的数组元素 ...

随机推荐

  1. 滚动时div的背景图片随之滚动

    在浏览一些网站时发现有一种效果是当滚动时看到某一DIV的背景也会随之滚动,如下: 当滚动时内容位置保持不变,但是内容后面的背景却在随着滚动.随之我通过审查元素看到了其是通过background-pos ...

  2. OPENCV图像变换-1

    图像变换是指将一幅图像变换为图像数据的另一种表现形式,例如将图像进行傅立叶变换,或者对图像进行X,Y方向的求导等,经过这些变换,可以将图像数据处理中的某些问题换一个别的角度想办法,所以图像变换是图像处 ...

  3. November 11th 2016 Week 46th Friday

    Keep in mind that neither success nor failure is ever final. 无论成败,皆非定局. The final is not coming, but ...

  4. Http协议与TCP协议理解

    TCP协议对应于传输层,而HTTP协议对应于应用层,从本质上来说,二者没有可比性.Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次Http请求.Http会通 ...

  5. HTML5 - Canvas动画样例(谷歌弹跳球)

    1,样例说明 (1)在没有鼠标介入的情况下,这些球就像有磁性一样拼成"Google"字样. (2)在鼠标移动到其中后,小球像是受到了排斥,向画布的四周扩散,然后不规则地反弹回来. ...

  6. STM32+NRF24L01无线(转)

    源:STM32+NRF24L01无线 硬件SPI和模拟SPI源码: nrf24发送(模拟SPI)BHS-STM32.rar nrf24接收(模拟SPI)BHS-STM32.rar nrf24发送(硬件 ...

  7. jQuery API 中文文档

    Reference: http://www.css88.com/jqapi-1.9/jQuery.proxy/

  8. 博客停更及OI退役公告

    停更&&OI退役 公告 高中OI之路就这样结束了,曾经想过回在NOI跪,APIO跪,HNOI跪却从未想过会在NOIP跪! 没办法自己作死啊,CCF感觉还是很良心的混个省一回来了,看以后 ...

  9. C++ CRTP singleton

    C++ CRTP 是个很有意思的东西,因为解释原理的文章很多,但是讲怎么用的就不是很多了. 今天就稍微写下CRTP(奇异递归模板模式)的一个有趣的用法:Singleton(单例模式) 单例有很多中写法 ...

  10. runat="server"

    加runat="server"表示该控件是服务器端控件,不加表示是客户端控件. runat="server"直接回交服务器,处理数据,又以数据加密后的hidde ...