• 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;
    View Code

    虽然 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);
    }
    View Code

    这也是为何我们在定义任务的时候,都会建议在 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;
    View Code

    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 还是有些复杂(或者说有点绕)的。

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

  • 相关阅读:
    【洛谷P6835】线形生物
    【洛谷P2679】子串
    【洛谷P5072】盼君勿忘
    【洛谷P3312】数表
    【洛谷P1447】能量采集
    【洛谷P2257】YY的GCD
    【洛谷P4318】完全平方数
    【AT2300】Snuke Line
    window.showModalDialog
    js typeof
  • 原文地址:https://www.cnblogs.com/vajoy/p/6359950.html
Copyright © 2020-2023  润新知