函数式编程

高阶函数

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

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

偏函数

通过指定部分参数来产生一个新的定制函数的形式就是偏函数

var isType = function (type) {
    return function (obj) {
        return toString().call(obj) == '[object' + type + ']'
    }
}

var isString = isType('String')
var isFunction = isType('Function')

异步编程解决方案

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

异步I/O调用示意图

  • 难点1 异常处理

    Node在处理一场上形成了一种约定,将异常作为回调函数的第一个实参传回,如果为空值,则表明异步调用没有异常跑出。在我们自行编写的异步方法上,也需要去遵循这样一些原则:

    • 原则一:必须执行调用者传入的回调函数
    • 原则二:正确传递回异常供调用者判断
  • 难点2 函数嵌套过深
  • 难点3 阻塞代码
  • 难点4 多线程编程
  • 难点5 异步转同步

主要解决方案如下3种

  • 事件发布/订阅模式
  • Promise/Deferred模式
  • 流程控制库

事件发布/订阅模式

事件发布/订阅模式可以实现一个事件与多个回调函数的关联,这些回调函数又称为事件侦听器。通过emit()发布事件后,消息会立即传递给当前事件的所有侦听器执行。侦听器可以很灵活地添加和删除,使得事件和具体处理逻辑之间可以很轻松的关联和解耦。

事件发布/订阅模式自身并无同步和异步调用的问题,但在Node中,emit()调用多半是伴随事件循环而异步触发的,所以我们说事件发布/订阅广泛应用于异步编程。

事件发布/订阅模式常常用来解耦业务逻辑,事件发布者无须关注订阅的侦听器如何实现业务逻辑,甚至不用关注有多少个侦听器存在,数据通过消息的方式可以很灵活地传递。在一些典型的场景中,可以通过事件发布/订阅模式进行组件封装,将不变的部分封装在组件内部,将容易变化,需自定义的部分通过事件暴露给外部处理,这是一种典型的逻辑分离方式。在这种事件发布/订阅式组件中,事件的设计非常重要,因为它关乎外部调用组件时是否优雅,从某种角度来说事件的设计就是组件的接口设计。

从另一个角度来看,事件侦听器模式也是一种hook,利用钩子导出内部数据或者状态给外部调用者。

利用事件队列解决雪崩问题

在事件订阅/发布模式中,通常也有一个once()方法,通过它添加的侦听器只能执行一次,在执行之后就会将它与事件的关联移除。这个特性常常可以帮助我们过滤一些重复性的事件响应。

多异步之间的协作方案

多个异步场景中回调函数的执行并不能保证顺序,且回调函数之间相互没有任何交集,所以需要借助一个第三方函数和第三方变量来处理异步协作的结果。通常,我们把这个用于检测次数的变量叫做哨兵变量。

Promise/Deferred模式

Promise和Deferred整体关系

Promise和Deferred整体关系

Promise是高级接口,事件是低级接口。低级接口可以构成更多更复杂的场景,高级接口一旦定义,不太容易变化,不再有低级接口的灵活性,但对于解决典型问题非常有效。

思考问题:Promise主要解决的是单个异步操作中存在的问题,当我们需要处理多个异步调用时又该如何处理呢?

var p1 = readFile('foo.txt', 'utf-8')
var p12 = readFile('bar.txt', 'utf-8')

var deferred = new Deferred()
deferred.all([p1, p2]).then (function(results) {
    //TODO
}, function(err) {
    //TODO
})

通过all()方法抽象多个异步操作,只有所有异步操作成功,这个异步操作才算成功,一旦其中一个异步操作失败,整个异步操作就失败。

支持序列执行的Promise

理想的编程体验应当是前一个的调用结果作为下一个调用的开始,是传说中的链式调用。

promise()
    .then(obj.api1)
    .then(obj.api2)
    .then(obj.api3)
    .then(obj.api4)
    .then(function (value4) {
        //TODO value4
    }, function(error) {
        //TODO error from step1 through setp4
    })
    .done()

要让promise支持链式执行,主要通过以下两个步骤

  • 将所有回调都存在队列中
  • promise完成时,逐个执行回调,一旦检测到返回了新的promise对象,停止执行,然后将当前的Deferred对象的promise引用改为新的promise对象,并将队列中余下的回调转交给它。

流程控制库

尾触发与next

有一类需要手工调用才能持续执行后续调用,我们将此类方法叫做尾触发。常见关键词是next()。应用最多的地方是Connect的中间件。

中间件通过队列形成一个处理流

async
  • series 实现一组任务的串行执行,适合无依赖的异步
  • parallel 并行执行异步操作
  • waterfall 执行存在依赖的异步操作
  • auto 自动分析依赖执行异步操作
step

接收任意数量的任务,所有的任务都将会串行一次执行。

wind

流程控制小结

事件发布/订阅模式相对算是一种较为原始的方式,Promise/Deferred模式贡献了一个非常不错的异步任务模型的抽象。上面几种异步流程控制方案与Promise/Deferred模式的思路不同,Promise/Deferred的重点在于封装异步的调用部分,流程控制库则显得没有模式,将处理重点放在回调函数的注入上。从自由度上来说,asyncsetp这类的流程控制库相对灵活得多。EventProxy库则主要借鉴事件发布/订阅模式和流程控制库通过高阶函数生成回调函数的方式实现。

异步并发控制

场景: 并发量过大时,下层服务器会吃不消,如果是对文件系统进行大量的并发调用,操作系统的文件描述符数量会被瞬间用光,抛出如下错误Error: EMFLIE, too many open files。可以看出,异步I/O与同步I/O的明显差距:同步I/O因为每个I/O都彼此阻塞,在循环体中,总有一个接着一个调用,不会出现耗用文件描述符太多的情况,同时性能也是低下的;对于异步I/O,虽然并发容易实现,但是由于太容易实现依然需要控制。换言之,尽管要压榨底层系统的性能,但还是要给予一定的过载保护。

bagpipe

  • 通过一个队列控制并发量
  • 如果当前活跃(调用发起但未执行回调)的异步调用量小于限定值,从队列中取出执行
  • 如果活跃调用达到限定值,调用暂时存放在队列中
  • 每个异步调用结束时,从队列中取出新的异步调用执行

async

  • parallelLimit 异步调用限制
  • queue 动态增加并行任务