上篇文章我们分别对 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. iOS开发之监听键盘高度的变化

    最近做的项目中,有一个类似微博中的评论转发功能,屏幕底端有一个输入框用textView来做,当textView成为第一响应者的时候它的Y值随着键盘高度的改变而改变,保证textView紧贴着键盘,但又 ...

  2. windows下查找指定端口被哪个程序占用

    在Windows环境下,用netstat命令查看某个端口号是否占用,为哪个进程所占用. eg.查看端口号为61078被哪个程序占用 1.查看端口号为61079被哪个PID所占用:Netstat –an ...

  3. 我推荐的一些iOS开发书单

    文/叶孤城___(简书作者)原文链接:http://www.jianshu.com/p/2fa080673842著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. 上次发了一下比较不错的i ...

  4. iOS纯代码手动适配 分类: ios技术 2015-05-04 17:14 239人阅读 评论(0) 收藏

    首先说下让自己的程序支持iPhone6和6+,第一种使用官方提供的launch screen.xib,这个直接看官方文档即可,这里不再多述:第二种方法是和之前iPhone5的类似,比较简单,为iPho ...

  5. MS SQL Server数据库修复/MDF数据文件数据恢复/MDF质疑/mdf无法附加

    微软的SQL Server 数据库最常用的有两种类型的文件: 1.主要数据文件,文件后缀一般是.MDF: 2.事务日志文件,文件后缀一般是.LDF. 用户数据表.视图.存储过程等等数据,都是存放在MD ...

  6. js生成随机数

    //生成n以内的随机数 function getRandom(n){ return Math.floor(Math.random()*n+1) } //生成1000以内的随机数 alert(getRa ...

  7. python3自动下载优酷视频小程序

    我们一般都在优酷里看一些好玩的视频,有时候看到精彩的就想下载到本地保存起来留作纪念,在win下可以用维棠等软件下载,但苦了用linux的孩子们.尽管chrome和firefox的一些插件可以下载,但有 ...

  8. c++初学(电梯实验)

    模拟电梯载人实验 Elevator.h class Elevator{public:    Elevator();    ~Elevator();    void getNowNum();       ...

  9. vue-cli 官方模板webpack-simple的npm run dev 与npm run bulid的一些问题

    npm run dev命令后并不会在dist目录下生成build.js文件,开发环境下build.js是在运行内存中的. 在package.json的scripts属性的dev后面加上--port 8 ...

  10. centos 6.5下安装、配置并启动SSH远程访问

    centos 6.5下安装.配置并启动SSH远程访问 1.登录centos 6.5系统,使用root用户登录,如果为非root用户则执行su或su - 或su root或su - root切换为roo ...