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 同步、异步执行顺序
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容

  • gulpjs是一个前端构建工具,与gruntjs相比,gulpjs无需写一大堆繁杂的配置参数,API也非常简单,学...
    依依玖玥阅读 3,147评论 7 55
  • gulpjs是一个前端构建工具,与gruntjs相比,gulpjs无需写一大堆繁杂的配置参数,API也非常简单,学...
    build1024阅读 527评论 0 0
  • gulpjs是一个前端构建工具,与gruntjs相比,gulpjs无需写一大堆繁杂的配置参数,API也非常简单,学...
    井皮皮阅读 1,289评论 0 10
  • 原文链接:www.talkingcoder.com 什么是gulp Gulp是可以自动化执行任务的工具。在平时开发...
    李2牛阅读 937评论 0 3
  • gulpjs是一个前端构建工具,与gruntjs相比,gulpjs无需写一大堆繁杂的配置参数,API也非常简单,学...
    小裁缝sun阅读 918评论 0 3