yeoman-generator 中的 run loop 实现

本文作者:ivweb qcyhust

导语

在上一篇yeoman(利用yeoman构建项目generator)的构建项目介绍文中提到过一个yeoman genenrator的run loop。当时提到“每一个添加进去的方法都会在generator调用的时候被调用,而且通常来讲,这些方法是按照顺序调用的”以及简单介绍了yeoman的方法执行顺序,这篇文章将仔细分析run loop的具体实现。

run loop

所谓的run loop是IOS开发中的一个概念,具体来说是一个与线程相对应的对象,用它来实现线程自动释放池、延迟回调、触摸事件、屏幕刷新等功能。线程一般在执行完任务后就直接退出,run loop这个循环会让线程处于接受消息->等待->处理的循环中,直到接受到退出的信号才会结束循环。 yeoman中的run loop概念是说存在多个generator时,在我们给每一个genenrator类都定义了一系列具有优先级关系的属性事件用于构建不同的项目文件,每一次实例化genenrator的时候运行我们的构建程序,多个generator的组合使用就需要一个run loop处理来接收用户发出的构建事件,等待用户输入,按优先级的顺序处理构建程序的循环。 参考Run Loops

核心库Grouped-queue

yeoman使用Grouped-queue来处理run loop。yeoman有自己的事件生命周期,在前文中提到过,按照顺序列出来是initializing,prompting,configuring,default,writing,conflicts,install,end,开发者在generator中定义的方法名如果不在上面列出的事件中,那么将作为defalut事件,在configuring和writing中间被调用。 Grouped-queue用来管理一个存在于内存中的任务队列,引用库后返回一个构造函数。 const queue = require('grouped-queue'); queue接受一个可配置的数组也就是任务组作为参数,任务组中的元素为字符串,第一个字符串将是第一个调用的任务,第二个字符串是第二个任务,以此类推。 实例queue有一个add方法add( [group], task, [options] ),向任务组中添加任务,参数:

  • 任务名
  • 任务方法
  • 配置对象 如果没有指定组的名字,会使用default。每一个任务方法都会收到一个callback作为参数,这个callback必须在定义的任务中被调用来进入下一个任务。// 向任务队列中增加writing任务、 queue.add('writing', function( cb ) { /* 一些完成一些事情,同步或异步, * 如果是同步则在最后调用cb * 如果是异步,则在异步回调中调用cb */ });

这样就可以构建一个任务队列,事件将按顺序被调用,每次调用add,队列都会执行一次:

const queue = new GroupedQueue(['first', 'second', 'thrid']);

queue.add('first', (callback) => {
    console.log('the first task1');
    callback();
});

queue.add('first', (callback) => {
    console.log('the first task2');
    callback();
});

queue.add('thrid', (callback) => {
    console.log('the thrid task');
    callback();
});

queue.add('second', (callback) => {
    console.log('the second task');
    setTimeout(callback, 1000);
});

// 按顺序输出 first1 first2 second thrid 任务

队列实现

Grouped-queue的源码并不复杂,其核心就是维持一个对列对象,这个对象已任务名为key,以任务数组为value,然后按照任务对列的顺序调用。Queue继承了EventEmitter对象的属性,可以利用订阅发布来调用事件。 先来看整体的实现(代码有删减):

function Queue( subQueues ) {
      this.queueNames = subQueues;

    // 存放任务数组
     this.__queues__ = {};

    subQueues.forEach(function( name ) {
           this.__queues__[name] = new SubQueue();
    }.bind(this));
}

function SubQueue() {
    // 每一个任务的子任务
      this.__queue__ = [];
}

可以看出Queue用私有属性queues对象维护任务队列,我们传入的任务队列在内部用于生成任务数组subQueue的queue数组,调用add方法实际上就是往queues对象中的相应key的任务数组中添加新的方法元素。上面的例子就会有如下结构

{
    first: [first task1, first task2]
    second: [second func]
    thrid: [thrid func]
}

在看看运行(代码有删减):

Queue.prototype.run = function() {
    if ( this.running ) return;
      this.running = true;

    this._exec(function() {
        this.running = false;
        // 任务队列中的任务都运行完就触发end事件
        if (_(this.__queues__).map('__queue__').flatten().value().length === 0) {
              this.emit('end');
        }
    }.bind(this));
};

Queue.prototype._exec = function( done ) {
      var pointer = -1;
    // 取出任务队列
    var names = Object.keys( this.__queues__ );

    var next = function next() {
        pointer++;
        // 没有任务了执行done
        if ( pointer >= names.length ) return done();
        // 执行相应key下的subQueue的run方法
        this.__queues__[ names[pointer] ].run( next.bind(this), this._exec.bind(this, done) );
    }.bind(this);

    next();
};

/*
* SubQueue实例的run方法
* $param skip 跳过执行Queue._exec的next
* $param done 指向Queue._exec,从头执行任务队列
*/
SubQueue.prototype.run = function( skip, done ) {
     // 如果数组中没有方法元素就跳过
      if ( this.__queue__.length === 0 ) return skip();
    // 取出数组中的方法,将done也就是callback作为第一个参数传入
      setImmediate( this.shift().task.bind(null, done) );
};

每次调用callback任务队列会重新执行一次。每次重新执行时,之前的任务的数据已经都是空的,所以会直接skip跳过,执行下一个next。每一个任务都是使用setImmediate在下一个事件循环中调用,Grouped Queue中添加了一个标志running,在run方法中判断,如果是runing状态则直接返回,不会调用exec,等到callback调用才会执行exec来保证执行顺序。

简单实现yeoman的生命周期

明白了Grouped Queue的使用方法后原理后可以简单模拟yeoman genenrator的生命周期实现,首先定义任务队列:

const queues = [
  'initializing',
  'prompting',
  'configuring',
  'default',
  'writing',
  'conflicts',
  'install',
  'end'
];

然后提供一个基类给每一个genenrator继承:

const GroupedQueue = require('grouped-queue');
const runAsync = require('run-async');

class Base {
    constructor() {
        this.runLoop = new GroupedQueue(queues);

        this.run();
    }

    run() {
        const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
        const isValidMethods = (method) => method.charAt(0) !== '_' && method !== 'constructor';
        // 过滤方法名
        const validMethods = methods.filter(isValidMethods);
        const self = this;

        validMethods.forEach((method) => {
            const taskFunc = this[method];
            let methodName = method;

            // 自定义方法归入defalut队列
            if(!queues.includes(method)) {
                methodName = 'default';
            }

            this.runLoop.add(methodName, (next) => {
                // 处理异步事件
                runAsync(function () {
                    return taskFunc.apply(self);
                })().then(next);
            });
        })
    }
}

实现一个genenrator类:

class Test extends Base {
    constructor(args) {
        super(args);

        console.log('constructor');
    }

    initializing() {
        return new Promise((done, reject) => {
            setTimeout(() => {
                console.log('initializing');
                done()
            }, 1000);
        })
    }

    writing() {
        console.log('writing');
    }

    selfdefine() {
        return new Promise((done, reject) => {
            setTimeout(() => {
                console.log('selfdefine');
                done()
            }, 2000);
        })
    }

    install() {
        console.log('install');
    }
}

const test = new Test();
// 输出  constructor
         initializing 隔1秒
         selfdefine   隔2秒
         writing
         install

原文出处:IVWEB社区

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏york技术分享

awk 使用教程 - 通读篇(30分钟入门)

很多刚接触awk,sed等命令时,看到帮助文档一堆参数,一堆符号感觉有点慌,我刚开始学习时也出现过这样的问题,这篇文章从我们工作遇到的问题出发,由浅入深,重点在...

65417
来自专栏about云

日志分析实战之清洗日志小实例5:实现获取不能访问url

问题导读 1.在url中,如何过滤不需要的内容? 2.如何获取404记录并且获取字段? 3.获取不能访问url列表的思路是什么? about云日志分析实...

3155
来自专栏Java开发者杂谈

JDK1.7新特性(3):java语言动态性之脚本语言API

简要描述:其实在jdk1.6中就引入了支持脚本语言的API。这使得java能够很轻松的调用其他脚本语言。具体API的使用参考下面的代码: 1 package...

28910
来自专栏微信公众号:Java团长

静态代理 VS 动态代理

1.通过DRP这个项目,了解到了动态代理,认识到我们之前一直使用的都是静态代理,那么动态代理又有什么好处呢?它们二者的区别是什么呢?

853
来自专栏丑胖侠

Zookeeper之开源客户端ZkClient

ZkClient是由Datameer的工程师开发的开源客户端,对Zookeeper的原生API进行了包装,实现了超时重连、Watcher反复注册等功能。 ZKC...

3015
来自专栏desperate633

设计模式之代理模式(Proxy模式)代理模式的引入代理模式的实例程序代理模式分析

Proxy是代理人的意思,指的是代替别人进行工作的人。当不一定需要本人亲自去做的工作的时候,就可以寻找代理人去完成。 但在代理模式中,往往是相反的,通常是代理...

812
来自专栏技术记录

Protobuf3语法详解

6515
来自专栏Hongten

Struts2 Action

  具体Action的实现可以是一个普通的java类,里面有public String execute方法即可

702
来自专栏北京马哥教育

Shell特殊变量和命令行参数详解

? 1.shell变量基础 shell变量是一种很“弱”的变量,默认情况下,一个变量保存一个串,shell不关心这个串是什么含义。 所以若要进行数学运算,必须...

3606
来自专栏DOTNET

【翻译】MongoDB指南/引言

【原文地址】https://docs.mongodb.com/manual/ 引言 MongoDB是一种开源文档型数据库,它具有高性能,高可用性,自动扩展性 1...

2286

扫码关注云+社区