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 条评论
登录 后参与评论

相关文章

来自专栏hbbliyong

winform 、WPF传值方式详解

1.构造函数 2.静态变量 3.增加窗体属性 public string name{set;get;} 例如: public partial class Wi...

3396
来自专栏我是攻城师

你不知道的Java的split的小问题

2706
来自专栏前端学习心得

JavaScript常见的继承方式

JS作为面向对象的弱类型语言,继承也是其非常强大的特性之一。那么在JS中常见的继承方式有几种呢?

622
来自专栏xingoo, 一个梦想做发明家的程序员

Java程序员的日常—— 垃圾回收中引用类型的作用

在Java里面,是不需要太过于关乎垃圾回收,但是这并不意味着开发者可以不了解垃圾回收的机制,况且在java中内存泄露也是家常便饭的事情。因此了解垃圾回收的相关...

17310
来自专栏Java与Android技术栈

高阶函数和Java的Lambda

java 8引入了函数式编程。函数式编程重点在函数,函数变成了Java世界里的一等公民,函数和其他值一样,可以到处被定义,可以作为参数传入另一个函数,也可以作为...

863
来自专栏

go 语言的序列化与反序列化

与c 语言一样, 在网络编程中, go语言同样需要进行序列化与反序列化 在c语言中, 通常需要一块内存缓冲区用来收 发数据。缓冲区一般定义成char *buff...

2767
来自专栏编程坑太多

Java 8 新特性 Lambda 表达式简单使用

1559
来自专栏java一日一条

Java习惯用法总结

551
来自专栏青枫的专栏

c语言基础学习07_指针

=============================================================================

320
来自专栏一直在跳坑然后爬坑

前往kotlin的路上

官网:http://kotlinlang.org/docs/reference/ github:https://github.com/JetBrains/ko...

731

扫码关注云+社区