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;
虽然 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 还是有些复杂(或者说有点绕)的。
本文的注释和示例代码可以从我的仓库上获取,读者可自行下载调试。共勉~