异步编程 | 程序小兵

异步编程

函数式编程

JavaScript中,函数作为一等公民,使用是哪个非常自由,无论调用它或者作为参数,或者作为返回值均可。函数的灵活性是JavaScript比较吸引人的地方之一,它与古老的Lisp颇具渊源。在通常的语言中,函数的参数只接受的数据类型或者对象引用,返回值也只是基本数据类型和对象引用。例如:

function foo(x){
    return x;
}

高阶函数则是把函数作为参数,或是将函数作为返回值的函数,例如:

function foo(x){
    return function(){
        return x;
    };
}

高阶函数可以将函数作为输入或者返回值的变化看起来虽细小,但是对于C/C++语言而言,通过指针也可以达到相同的效果。但是高阶函数比普通函数要灵活许多,除了通过通常意义的函数调用返回外,还形成了一种后续传递风格(continuation passing style)的结果接收方式,而非单一的返回值形式。后续传递风格的程序编写将函数的业务重点从返回值转移到回调函数中:

function foo(x, bar){
    return bar(x);
}

以上的代码为例,对于相同的foo函数,传入的bar参数不同,则可以得到不同的效果。例如:

var points = [40, 100, 1, 4, 25, 10];
points.sort(function(a,b){
    return a - b;
});

通过改动sort函数的参数,可以决定不同排序方式,从这里可以看出高阶函数的灵活性。

因此,异步编程有以下优点与难点。

  • 优点

node带来的最大的特性莫过于基于事件驱动的非阻塞I/O模型,这是它的灵魂所在。非阻塞I/O可以是CPU和I/O并不互相依赖等待,让资源得到更好的利用。对于网络而言,并行带来的想象空间更大,延展而开的是分布式和云。并行是得各个单点之间能够更有效的组织起来,这也是node在云技术厂商广受喜欢的原因。

  • 缺点

当然,异步的编程也带来了很多缺点。例如,异常处理,函数嵌套太深,阻塞代码,多线程编程等。我简单说下阻塞代码的缺点:

对于一开始接触JavaScript的开发者,比较无语的是没有sleep()函数功能,唯独能使用的是延时操作的setInterval()setTimeout()函数。但是让人抓急的是,这2个函数并不能阻塞后续代码的持续执行。所以多半的开发者会有这样一段代码来实现sleep(1000)的效果:

var start = new Date();
while(new Date() - start < 1000){
    //TODO
}

解决

异步编程主要解决方案如下:

  • 事件发布/订阅模式
  • promise/deferred模式
  • 流程控制库

    事件发布/订阅模式

    事件监听模式是一种广泛的异步编程的模式,是回调函数的事件化,又称为发布/订阅模式。node提供了events模块是发布\订阅模式的一个简单实现。例如:

//订阅
emitter.on("event1", function(message){
    console.log('message:'+ message);
});
//发布
emitter.emit("event1", 'i am message');

Promise/Deferred模式

流程控制库模式

除了事件和promise外,还有一类方法是需要手工调用才能持续执行后续调用的,我们将此类方法饺子尾触发,常见关键字为next.事实上目前应用最多的是connect中间件。例如Connect的API暴露方式:

var app = connect();
//middleware
app.use(connect.staticCache());
app.use(connect.static(_dirname+'/public'));
app.use(connect.query());
//...

app.listen(3001);

在通过use()方法注册好一系列中间件后,监听端口上请求。中间件利用尾触发的机制,最简单的中间件如下:

function(req, res, next){
    //中间件
}

中间件机制使得在处理网络请求时,可以像面向切面编程一样进行过滤、验证、日志等功能,而不与具体业务逻辑产生关联,以致产生耦合。
具体看下Connect的核心实现:

function createServer(){
    function app(req, res){
        app.handle(req, ers);
    }
    utils.merge(app, proto);
    utils.merge(app, EventEmitter.prototype);
    app.route = '/';
    app.stack = [];
    for(var i = 0; i < arguments.length; ++i){
        app.use(arguments[i]);
    }
    return app;
}

真正的核心代码是app.stack = [];这句。stack的属性是这个服务器内部维护的中间件队列。通过调用use()方法我们可以将中间件放进队列中。如下为use()方法的重要部分:

app.use = function(route, fn){
    this.stack.push({route:route, handle:fn});
    return this;
};

结合node原生的http模块实现监听即可。监听函数实现如下:

app.listen = function(){
    var server = http.createServer(this);
    return server.listen.apply(server, arguments);
}

最终回到app.handle()函数,每一个监听到的网络请求都将从这里开始处理:

app.handle = function(req, res, out){
    //todo
    next();
}

最后关节的next()方法,我们简化下:

function next(err){
    //todo
    //next callback
    layer = stack[index++];
    layer.handle(req, res, next);
}

因此,所有嫌异步编程复杂的开发者均可以参考connect的流式处理。但是,如果每个步骤都采用异步来完成,实际上此模式的处理只是串行化的处理,没办法通过并行的异步调用来提升业务的处理效率。流式处理可以将一些串行的逻辑扁平化,但是并行逻辑处理还是需要搭配事件或者Promise完成,这样在纵向和横向都能够各自清晰。

其中,最著名的流式处理控制模块则为async, 其次则是step.

完。

文章目录
  1. 1. 函数式编程
  2. 2. 解决
    1. 2.1. 事件发布/订阅模式
    2. 2.2. Promise/Deferred模式
    3. 2.3. 流程控制库模式
,