gulp源码解析(三)—— 任务管理

上篇文章我们分别对 gulp 的 .src 和 .dest 两个主要接口做了分析,今天打算把剩下的面纱一起揭开 —— 解析 gulp.task 的源码,了解在 gulp4.0 中是如何管理、处理任务的。

在先前的版本,gulp 使用了 orchestrator 模块来指挥、排序任务,但到了 4.0 则替换为 undertaker 来做统一管理。先前的一些 task 写法会有所改变:

更多变化点,可以参考官方 changelog,或者在后文我们也将透过源码来介绍各 task API 用法。

o_div

从 gulp 的入口文件来看,任务相关的接口都是从 undertaker 继承:

接着看 undertaker 的入口文件,发现其代码粒化的很好,每个接口都是单独一个模块:

o_div

我们先从构造函数入手,可以知道 undertaker 其实是作为事件触发器(EventEmitter)的子类:

这意味着你可以在它的实例上做事件绑定(.on)和事件触发(.emit)处理。

另外在构造函数中,定义了一个内部属性 _registry 作为寄存器(注册/寄存器模式的实现,提供统一接口来存储和读取 tasks)

寄存器默认为 undertaker-registry 模块的实例,我们后续可以通过其对应接口来存储和获取任务:

undertaker-registry 的源码也简略易懂:

o_div

虽然 undertaker 默认使用了 undertaker-registry 模块来做寄存器,但也允许使用自定义的接口去实现:

此处的 this.registry 接口提供自 lib/registry 模块:

o_div

接着看剩余的接口定义:

其中 registry 是直接引用的 lib/registry 模块接口,在前面已经介绍过了,我们分别看看剩余的接口(它们均存放在 lib 文件夹下)

o_div

1. this.task

为最常用的 gulp.task 接口提供功能实现,但本模块的代码量很少:

其中第一段 if 代码块是为了兼容如下写法:

第二段 if 是对传入的 fn 做判断,为空则直接返回 name(任务名称)对应的 taskFunction。即用户可以通过 gulp.task(taskname) 来获取任务方法。

此处的 _getTask 接口不外乎是对 this._registry.get 的简单封装。

o_div

2. this._setTask

名称加了下划线的一般都表示该接口只在内部使用,API 中不会对外暴露。而该接口虽然可以直观了解为存储 task,但它其实做了更多事情:

这里的 helpers/metadata 模块其实是借用了 WeakMap 的能力,来把一个外部无引用的 taskFunction 对象作为 map 的 key 进行存储,存储的 value 值是一个 metadata 对象。

metadata 对象是用于描述 task 的具体信息,包括名称(name)、原始方法(orig)、依赖的任务节点(tree.nodes)等,后续我们即可以通过 metadata.get(task) 来获取指定 task 的相关信息(特别是任务依赖关系)了。

o_div

3. this.parallel

并行任务接口,可以输入一个或多个 task:

该接口会返回一个带有依赖关系 metadata 的 parallelFunction 供外层 task 接口注册任务:

这里有两个最重要的地方需要具体分析下:

我们先看下 createExtensions 接口:

故 extensions 变量获得了这样的一个对象:

如果我们能把它们跟每个任务的创建、执行、错误处理过程关联起来,例如在任务执行之前就调用 extensions.after(curTaskStorage),那么就可以把扩展对象 extensions 的属性方法作为任务各生命周期环节对应的回调了。

做这一步关联处理的,是这一行代码:

其中“create”引用自 bach/lib/parallel 模块,除了将扩展对象和任务关联之外,它还利用 async-done 模块将每个 taskFunction 异步化,且安排它们并行执行:

o_div

首先介绍下 async-done 模块,它可以把一个普通函数(传入的第一个参数)异步化:

执行结果:

561179-20170205213805198-896340355

那么很明显,undertaker(或 bach) 最终是利用 async-done 来让传入 this.parallel 接口的任务能够异步去执行(互不影响、互不依赖)

561179-20170208202429791-1024456682

o_div

我们接着回过头看下 bach/lib/parallel 里最重要的部分:

nowAndLater 即 now-and-later 模块,其 .map 接口如下:

在这段代码的 map 方法中,通过 for 循环遍历了每个传入 parallel 接口的 taskFunction,然后使用 iterator(async-done)将 taskFunction 异步化并执行(执行完毕会触发 hadler),并将 extensions 的各方法和 task 的生命周期关联起来(比如在任务开始时执行“start”事件、任务出错时执行“error”事件)

o_div

这里还需留意一个点。我们回头看 async-done 的示例代码:

async-done 支持要异步化的函数,通过执行传入的回调来通知 async-done 当前方法可以结束并执行回调了:

所以问题来了 —— 每次定义任务时,都需要传入这个回调参数吗?即使传入了,要在哪里调用呢?

其实大部分情况,都是无须传入回调参数的。因为咱们常规定义的 gulp 任务都是基于流,而在 async-done 中有对流(或者Promise对象等)的消耗做了监听(消耗完毕时自动触发回调)

这也是为何我们在定义任务的时候,都会建议在 gulp.src 前面加上一个“return”的原因:

o_div

另外还有一个遗留问题 —— bach/parallel 模块中返回函数里的“done”参数是做啥的呢:

我们先看 now-and-later.map 里是怎么处理 done 的:

可以看出这个 done 不外乎是所有传入任务执行完毕以后会被调用的方法,那么它自然可以适应下面的场景了:

即 taskC 里的“done”将在定义 taskE 的时候,作为通知 async-done 自身已经执行完毕了的回调方法。

o_div

4. this.series

串行任务接口,可以输入一个或多个 task:

series 接口的实现和 parallel 接口的基本是一致的,不一样的地方只是在执行顺序上的调整。

在 parallel 的代码中,是使用了 now-and-later 的 map 接口来处理传入的任务执行顺序;而在 series 中,使用的则是 now-and-later 的 mapSeries 接口:

通过改动 next 的位置,可以很好地要求传入的任务必须一个接一个去执行(后一个任务在前一个任务执行完毕的回调里才会开始执行)

o_div

5. this.lastRun

这是一个工具方法(有点鸡肋),用来记录和获取针对某个方法的执行前/后时间(如“1426000001111”)

底层所使用的是 last-run 模块,代码太简单,就不赘述了:

o_div

6. this.tree

这是看起来不起眼(我们常规不需要手动调用到),但是又非常重要的一个接口 —— 它可以获取当前注册过的所有的任务的 metadata:

执行结果:

561179-20170208235905885-1215567678

那么通过这个接口,gulp-cli 就很容易知道我们都定义了哪些任务、任务对应的方法是什么、任务之间的依赖关系是什么(因为 metadata 里的“nodes”属性表示了关系链)。。。从而合理地为我们安排任务的执行顺序。

其实现也的确很简单,我们看下 lib/tree 的源码:

不外乎是遍历寄存器里的任务,然后取它们的 metadata 数据来返回,简单粗暴~

自此我们便对 gulp 是如何组织任务执行的原理有了一番了解,不得不说其核心模块 undertaker 还是有些复杂(或者说有点绕)的。

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

1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部