Gulp.task() 源码简析

前段时间一直在用 Webpack + Vue 开发 Web 应用,虽然使用了脚手架,但是 Webpack 繁琐的配置一直让我头疼。直到有个前端朋友推荐我去学习下 Gulp,我屁颠屁颠地去了解下。

简介

Gulp 是一款前端构建工具,无需写一大堆繁杂的配置参数,API也非常简单,学习起来很容易,如果你没接触过该款工具,请您学习后再读会比较容易。

举个栗子:

var gulp = require('gulp')
gulp.task('one', function(cb) {
    setTimeout(() => {
        console.log('one is done')
        cb()
    }, 2000);
})
gulp.task('two', ['one'], function() {
    console.log('two is done')
})
  • gulp 能按依赖、同步、异步确保 task 执行顺序,那么调用 gulp.task() 时 gulp 都干了些什么;
  • 怎么实现任务间的依赖以及任务的同步、异步处理

版本

Gulp v3.9.1

简析 Gulp

查看 ./node_modules/gulp/index.js

var Orchestrator = require('orchestrator');

function Gulp() {
  Orchestrator.call(this);
}
util.inherits(Gulp, Orchestrator);

var inst = new Gulp();
module.exports = inst;

很明显 Gulp 是继承 Orchestrator 的,并且 exports 是个实例对象,因此每当 require() 后变量是全局单例。其中有行代码:

Gulp.prototype.task = Gulp.prototype.add;

Gulp 的 task 函数是 add 函数的别名,然而在当前模块 Gulp 原型中并没有找到 add 函数的定义,很可能是继承 Orchestrator 原型中的定义,所有 Orchestrator 是 Gulp 的核心模块。

详析 Orchestrator

Git:https://github.com/robrich/orchestrator

A module for sequencing and executing tasks and dependencies in maximum concurrency
翻译:在 最大并发性 中排序和执行任务及依赖关系的模块

查看 ./node_modules/orchestrator/index.js
var util = require('util');
var events = require('events');
var EventEmitter = events.EventEmitter;

var Orchestrator = function () {
    EventEmitter.call(this);
    this.doneCallback = undefined;
    this.seq = [];
    this.tasks = {};
    this.isRunning = false;
};
util.inherits(Orchestrator, EventEmitter);

module.exports = Orchestrator;

很明显 Orchestrator 是继承 EventEmitter,所以 Gulp 具有事件监听和事件触发的功能。

Orchestrator 上定义了 4 个重要的属性:
  1. doneCallback:回调函数,当所有的任务完成是被调用
  2. seq:执行链(以最大并发能力执行的关键)
  3. tasks:用户定义的所有任务配置信息的集合
  4. isRunnning:标志位,表示当前是不是正在执行任务
add() 函数定义
Orchestrator.prototype.add = function (name, dep, fn) {
    ... // 初始化值以及参数的校验
    this.tasks[name] = {
        fn: fn,
        dep: dep,
        name: name
    };
    return this;
};

属性 tasks 类似 Map 存储着每个任务的名称、依赖以及执行函数等等。

开始执行任务

一般情况下在控制台输入 gulp [task] 开始执行任务,那么入口函数在哪里呢?
在源码中不难发现 Orchestrator.prototype.start = function() { ... },看函数名就知道是启动函数,这可以验证

验证入口函数

在 npm 本地仓库目录下 ./gulp.cmd 源码:

@IF EXIST "%~dp0\node.exe" (
  "%~dp0\node.exe"  "%~dp0\node_modules\gulp\bin\gulp.js" %*
) ELSE (
  @SETLOCAL
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  node  "%~dp0\node_modules\gulp\bin\gulp.js" %*
)

很明显运行了 node ./node_modules/gulp/bin/gulp.jsgulp.js 是入口 Js 文件,继续 gulp.js 部分源码:

var argv = require('minimist')(process.argv.slice(2));

var tasks = argv._; // 控制台 gulp [task] 的 task 名称数组
var toRun = tasks.length ? tasks : ['default']; // 若没有指定 task,则按照默认值

var cli = new Liftoff({
  name: 'gulp',
  ...
});
cli.launch({
  cwd: argv.cwd,
  ...
}, handleArguments);

function handleArguments(env) {
  ...
  var gulpInst = require(env.modulePath); // 关键点:导入模块实例对象,也就是 gulp
  ...
  process.nextTick(function() {
    ...
    // 这里就调用了入口方法
    gulpInst.start.apply(gulpInst, toRun); // 调用了 gulp 对象的 start 方法
  });
}
启动函数 start() 做了些啥
Orchestrator.prototype.start = function() {
    var args, arg, names = [], seq = [];
    args = Array.prototype.slice.call(arguments, 0);
    ... // 省略掉参数初始化以及校验 
    if (this.isRunning) {
        // 如果当前任务正在执行,则只结束并重置用户指定启动的任务
        this._resetSpecificTasks(names);
    } else {
        // 如果当前没有任务执行则重置所有的任务
        this._resetAllTasks();
    }
    if (this.isRunning) {
        // 如果您再次调用start(),而之前的运行仍在运行中
        // 将新任务预先添加到现有任务队列中
        names = names.concat(this.seq);
    }
    ...
    seq = [];
    try {
        this.sequence(this.tasks, names, seq, []); // 计算好任务作业链,这是实现最大并发性的关键函数
    } catch (err) {
        ...
        return this;
    }
    this.seq = seq;
    this.emit('start', {message:'seq: '+this.seq.join(',')}); // 触发 start 事件
    if (!this.isRunning) {
        this.isRunning = true;
    }
    this._runStep();
    return this;
};

变量 names 保存用户指定执行的 tasks 名称和任务链中为还未执行的 tasks 名称;
简单点说步骤:

  1. 该函数先检查是否正在执行 tasks,如果正在执行并且正在执行的 tasks 中有用户指定执行的 tasks,则停止并重置这些 tasks,然后将之前未指定的任务链(队列)重新加到新任务链中;如果没有执行任务,则重置所有定义的 tasks 的状态。
  2. 调用 sequence(),计算作业链,用于计算机按序执行任务(下面讲到)
  3. 触发 start 事件
  4. 调用 _runStep(),执行作业链中的任务
任务作业链是怎么计算的

答案在 sequencify 模块中,使用了简单的递归算法,见源码:

var sequence = function (tasks, names, results, nest) {
    var i, name, node, e, j;
    nest = nest || [];
    for (i = 0; i < names.length; i++) {
        name = names[i];
        // de-dup results
        if (results.indexOf(name) === -1) {
            node = tasks[name];
            if (!node) {
                e = new Error('task "'+name+'" is not defined');
                e.missingTask = name;
                e.taskList = [];
                for (j in tasks) {
                    if (tasks.hasOwnProperty(j)) {
                        e.taskList.push(tasks[j].name);
                    }
                }
                throw e;
            }
            if (nest.indexOf(name) > -1) {
                nest.push(name);
                e = new Error('Recursive dependencies detected: '+nest.join(' -> '));
                e.recursiveTasks = nest;
                e.taskList = [];
                for (j in tasks) {
                    if (tasks.hasOwnProperty(j)) {
                        e.taskList.push(tasks[j].name);
                    }
                }
                throw e;
            }
            if (node.dep.length) {
                nest.push(name);
                sequence(tasks, node.dep, results, nest); // recurse
                nest.pop(name);
            }
            results.push(name);
        }
    }
};

module.exports = sequence;

该函数有去重,形参 results 是排序后的结果
举个栗子:

  1. 任务 A 依赖任务 B、C(依赖任务有序)
  2. 任务 C 依赖任务 D
  3. 任务 E 依赖任务 F
  4. 控制台输入 gulp E A

计算后任务链顺序:F -> E-> B -> D -> C -> A

准备执行任务链 _runStep()

这个函数简单不贴源码,它做的事情:

  1. 遍历 seq 任务链,依次获取 task 配置信息
  2. 依次校验准备执行的 task 的状态以及所有依赖 tasks 的状态
  3. 依次调用 _runTask() 才准备执行任务
  4. 若全部 tasks 完成,调用 doneCallback() 回调函数
准备执行单个任务 _runTask()

步骤:

  1. 触发 task_start 事件
  2. 设置当前 task 执行标志为 true
  3. 重点:调用 runTask(fn, finishCallback) 真正执行 task,其中参数 fn 就是定义 task 时传入的任务函数,回调函数 finishCallback 做了三件事:
    1. 设置 task 为已完成、未执行
    2. 如果 task 执行中未抛出异常,触发 task_stop 事件;抛出异常,触发 task_err 事件
    3. 如果 task 执行中抛出异常,停止所有任务,触发 err 事件
    4. 若前三步正常(未抛异常),调用 _runStep 方法准备执行任务链下个 task
怎么处理同步、异步任务

答案在 runTask() 方法中,源码:

var eos = require('end-of-stream');
var consume = require('stream-consume');

module.exports = function (task, done) {
    var that = this, finish, cb, isDone = false, start, r;
    finish = function (err, runMethod) {
        var hrDuration = process.hrtime(start);

        if (isDone && !err) {
            err = new Error('task completion callback called too many times');
        }
        isDone = true;

        var duration = hrDuration[0] + (hrDuration[1] / 1e9); // seconds

        done.call(that, err, {
            duration: duration, // seconds
            hrDuration: hrDuration, // [seconds,nanoseconds]
            runMethod: runMethod
        });
    };
    cb = function (err) {
        finish(err, 'callback');
    };

    try {
        start = process.hrtime();
        r = task(cb);
    } catch (err) {
        return finish(err, 'catch');
    }

    if (r && typeof r.then === 'function') {
        // wait for promise to resolve
        // FRAGILE: ASSUME: Promises/A+, see http://promises-aplus.github.io/promises-spec/
        r.then(function () {
            finish(null, 'promise');
        }, function(err) {
            finish(err, 'promise');
        });
    } else if (r && typeof r.pipe === 'function') {
        // wait for stream to end
        eos(r, { error: true, readable: r.readable, writable: r.writable && !r.readable }, function(err){
            finish(err, 'stream');
        });
        // Ensure that the stream completes
        consume(r);
    } else if (task.length === 0) {
        // synchronous, function took in args.length parameters, and the callback was extra
        finish(null, 'sync');
    }
};

最主要的是 finish() 函数用来通知当前 task 执行结束。

  • 当 task 有异步操作时,我们想等待异步任务中的异步操作完成后再执行后续的任务怎么做么?

    1. 在异步操作完成后执行一个回调函数来通知 gulp 这个异步任务已经完成
    cb = function (err) {
        finish(err, 'callback');
    };
    r = task(cb);
    
    1. 定义任务时返回一个流对象
    r = task(cb);
    if (r && typeof r.pipe === 'function') {
        eos(r, { error: true, readable: r.readable, writable: r.writable && !r.readable }, function(err){
            finish(err, 'stream');
        });
        // Ensure that the stream completes
        consume(r);
    }
    
    1. 返回一个promise对象
    r = task(cb);
    if (r && typeof r.then === 'function') {
        r.then(function () {
             finish(null, 'promise');
        }, function(err) {
             finish(err, 'promise');
        });
    }
    
  • 当 task 没异步操作时(通过 task.length0 表示未定义回调函数第一个参数),主动调用 finish() 通过结束,并指定运行方法为 同步

    r = task(cb);
    if (task.length === 0) {
        finish(null, 'sync');
    }
    

总结

  • Gulp 继承 Orchestrator 实现了依赖、排序执行任务
  • 模块 sequencify 使用递归算法,其实现任务去重、依赖排序,最后生成作业链,它是使得 以最大并发性执行 成为可能,同时确保了依赖间的执行顺序
  • 模块 runTask 的方法 runTask() 确保了 task 同步、异步执行顺序

推荐阅读更多精彩内容