Node -- 异步编程

来源:互联网 发布:coc野猪升级数据 编辑:程序博客网 时间:2024/05/21 22:54

函数式编程

在JS中,函数为一等公民。函数可以作为参数/返回值。

高阶函数

高阶函数就是把其他函数作为参数或者返回值的函数。

栗子:

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

结合Node提供的最基本的事件模块可以看到,事件的处理方式正是基于高阶函数的特性来完成的。在自定义事件实例中,通过为事件注册不同的回调函数,可以很灵活的处理业务逻辑:

var emitter = new events.EventEmitter(); emitter.on('event_foo', function () {    // TODO });

常见的高阶函数:forEach()、 map()、reduce()、reduceRight()、filter()、every()、some()

偏函数

偏函数用法是指,创建一个调用另外一个部分——参数或者变量已经预置的函数——的函数的用法。

偏函数在使用时可能涉及闭包、函数柯里化以及高阶函数的知识。

栗子:

var isType = function (type) {     return function (obj) {        return toString.call(obj) == '[object ' + type + ']'; };    };var isString = isType('String'); var isFunction = isType('Function');

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

另外一个栗子:

_.after = function(times, func) {     if (times <= 0) return func();     return function() {        if (--times < 1) {             return func.apply(this, arguments);         }     };};

这个函数可以根据传入的times和具体方法,生成一个需要调用多次才能真正执行某个函数的函数。

异步编程的优势和难点

优势

Node带来的最大特性莫过于基于事件驱动的非阻塞IO模型,这是它的灵魂所在。非阻塞IO可以使CPU和IO并不互相依赖等待,让资源得到更好的利用。

这里写图片描述

利用事件循环的原理,JS线程像一个分配任务和处理结果的管家,IO线程池里的各个IO都是小二,负责完成被分配的任务。小二和管家之间互不依赖,所以可以保持整体的高效率。

这个模型的缺点在于,管家也就是CPU无法承担过多细节性任务,如果承担太多,则会影响到任务的调度。管家忙个不停,小二却得不到活儿干,结局则是整体效率的降低。

换言之,Node是为了解决编程模型中阻塞IO的性能问题的。采用单线程的模型,这导致Node更像是一个处理IO密集问题的能手,而CPU密集型则取决于管家的能耐如何。

得益于V8的Node可以算一流高手,在具备调用C/C++扩展模块的时候,Node的能力则可以逼近顶尖之列。

难点

异常处理

我们在处理异常的时候,通常指用类Java的try/catch语句进行异常捕获。

但是这种代码对于异步编程而言并不一定适用。异步IO的实现主要包含两个阶段:提交请求和处理结果。这两个阶段中间有事件循环的调度,两者彼此不关联。异步方法则通常在第一个阶段提交请求后立即返回,因为一场并不一定发生在这个阶段,所以try/catch在此处不会发挥任何作用。

例如,异步方法:

var async = function (callback) {     process.nextTick(callback);};

调用这个方法后,callback被存放起来,直到下一个事件循环才会被取出来执行。尝试对异步方法进行try catch智能捕获当次事件循环内的异常。对callback执行时抛出的异常无能为力。

解决方案

Node在处理异常上形成了一种约定,将异常作为回调函数的第一个实参传回,如果为空值,则表明异步调用没有异常。

在我们自行编写的异步方法上,也要去遵循这样的一些原则:
1、必须执行调用者传入的回调函数
2、正确传递回异常供调用者判断

var async = function (callback) {     process.nextTick(function() {        var results = something;         if (error) {            return callback(error);         }        callback(null, results);     });};

错误的方式:

try {    req.body = JSON.parse(buf, options.reviver);     callback();} catch (err){      err.body = buf;     err.status = 400;      callback(err);}

错误原因:不能对回调函数进行异常捕获。

正确的写法:

try {    req.body = JSON.parse(buf, options.reviver);} catch (err){     err.body = buf;    err.status = 400;    return callback(err); }callback();

只要将异常正确的通过回调函数传回即可。

函数嵌套过深

在Node中,事务存在多个异步调用的场景比比皆是。不如一个遍历目录的操作:

fs.readdir(path.join(__dirname, '..'), function (err, files) {              files.forEach(function (filename, index) {        fs.readFile(filename, 'utf8', function (err, file) {             // TODO        });     });});

在上述场景中,由于两次操作存在依赖关系,函数嵌套的行为也许情有可原。

在网页渲染中,通常需要数据、模板、资源文件,这三者也许互相之间并不依赖,但是最终的渲染结果中三者缺一不可:

fs.readFile(template_path, 'utf8', function (err, template) {     db.query(sql, function (err, data) {        l10n.get(function (err, resources) {             // TODO        });     });});

这在结果上保证没问题,但是,这没有利用好异步IO的并行优势。
这是异步编程的典型问题。我们在后文中会介绍如何解决。

阻塞代码

在JS中,没有sleep()这样的线程沉睡功能,唯独能用于延时操作的挚友setInterval()和setTimeout()这两个函数,并且这两个函数并不能阻塞后续代码的持续执行。

有很多开发者会写出这样的代码来实现sleep功能:

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

但是这段代码会持续占用CPU,与真正的沉睡线程相去甚远,完全破坏了事件循环的调度。由于Node单线程的原因,CPU资源全都会用于为这段代码服务,导致其余任何请求都得不到相应。

多线程编程

这里写图片描述

Node借鉴了浏览器的Web Workers模式,使用child_process作为其基础API,cluster模块是更深层次的应用。

异步转同步

Node提供了绝大部分的异步API,偶尔出现的同步需求将会因为没有同步API让开发者忽然无所适从。目前,Node中的同步式编程并不能得到原声支持,需要借助库或者编译等手段来实现。但对于异步调用, 通过良好的流程控制,还是能够将逻辑梳理成顺序式的形式。

异步编程的解决方案

事件发布/订阅模式

回调函数事件化。

Node自身提供的events模块是发布/订阅模式的一个简单实现。这个模块不存在事件冒泡,也不存在preventDefault()、stopPropagation()、stopImmediatePropagation()(http://www.css88.com/jqapi-1.9/event.stopImmediatePropagation/)等控制事件传递的方法。它有 addListener/on()、once()、removeListener()、removeAllListeners()、emit()等基本的事件监听模式的方法实现。

例子:

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

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

事件发布/订阅模式经常用来解耦业务逻辑,事件发布者无须关注订阅的侦听器如何实现业务逻辑,甚至不必关心有多少个侦听器的存在,数据通过消息的方式可以灵活的传递。在一些典型场景中,可以通过事件发布/订阅模式进行组建封装,将不变的部分封装在组建内部,将容易变化,需要自定义的部分通过事件暴露给外部处理。

Node对事件发布/订阅做了一些额外的处理:
1、如果对一个事件添加了超过10个侦听器,将会得到一条警告。

2、为了处理异常,EventEmitter对象对error事件进行了特殊对待。如果运行期间的错误触发了error事件,Event Emitter会检查是否有对error 事件添加侦听器,如果添加了,这个错误将交给侦听器处理,否则这个错误将会作为异常抛出。如果外部没有捕获这个异常,将会引起线程退出。

Promise/Deferred模式

使用时间的方式时,执行流程需要被预先设定。这是由发布/订阅模式的运行机制所决定的。

那么,是否有一种先执行异步调用,延迟传递处理的方式呢?

答案就是Promise/Deferred

使用前:

$.get('/api', {     success: onSuccess,     error: onError,      complete: onComplete});

使用后:

$.get('/api')    .success(onSuccess)     .error(onError)      .complete(onComplete);

这使得即使不调用这些方法,Ajax也会执行,这样的调用比预先需要传入回调让人觉得更舒适一些。在原始的API中,一个事件只能处理一个回调,而通过Deferred对象,可以对事件加入任意的业务处理逻辑:

$.get('/api').success(onSuccess1).success(onSuccess2);

Promise/Deferred模式被抽象为一个协议草案,发布在Common JS规范中。现在,CommonJS已经抽象出了Promises/A、Promises/B、Promises/D这样典型的一部模型。

Promise/Deferred在一定程度上可以缓解深度嵌套的问题。

promise/A

PromiseA的行为:

1、Promise操作只会处于三种状态中的一种:未完成,完成,失败。
2、Promise的状态只能出现从未完成到完成或者失败的转化,不能逆反。完成和失败这两个状态不能相互转化。
3、Promise的状态一旦转化,将不能被更改。

这里写图片描述

在API的定义上,PromiseA的提议比较简单,一个Promise对象只要具备then方法即可。
对then方法的要求如下:
1、接受完成态、错误态的回调方法。
2、可选的支持progress事件回调作为第三个方法。
3、then方法只接受function对象,对其他对象忽略。
4、then方法继续返回promise对象,以实现链式调用。

then方法定义如下:

then(fulfilledHandler, errorHandler, progressHandler)

then()方法所做的事情是将回调函数存放起来。为了完成整个流程,还需要触发执行这些回调函数的地方,实现这些功能的对象被称为Deferred。

使用promise前:

res.setEncoding('utf8'); res.on('data', function (chunk) {    console.log('BODY: ' + chunk); });res.on('end', function () {     // Done});res.on('error', function (err) {    // Error });

上述代码可以转化为:

res.then(function () {     // Done}, function (err) {     // Error}, function (chunk) {     console.log('BODY: ' + chunk);});

API相关代码:
这里写图片描述

Deferred主要用于内部,用于维护异步模型的状态,Promise则主要用于外部,通过then方法暴露给外部以添加自定义逻辑。

Promise的多异步操作

例如,对于多次文件读取饿场景,all()方法可以将两个单独的Promise重新抽象组合成一个新的Promise。只有所有的异步操作成功,这个异步操作才算成功, 一旦一个失败了,整个异步操作就失败了。

流程控制库

这一节将回介绍一些非模式化的应用, 虽非规范,但更灵活。

尾触发和Next

async

async提供了series方法来实现一组任务的串行执行;当我们需要并行时,async提供了parallel方法。

series方法适合无依赖的异步串行执行,但是如果当前一个结果时后一个调用的输入时,series方法就无法满足需求了。在这种场景下,应该使用waterfall方法。

除此之外,async提供了一个强大的方法auto来实现复杂的业务处理。

step

wind

异步并发控制

如果并发量太大,下层的服务器将回吃不消。例如,如果对文件系统进行大量的并发调用,操作系统的文件描述符的数量将瞬间被用光,会抛出错误:Error: EMFILE, too many open files。

因此,对于异步IO,虽然并发容易实现,但是由于太容易实现,依然需要控制——尽管需要压榨底层系统的性能,但还是需要给予一定的过载保护,防止过犹不及。

bagpipe的解决方案

bagpipe模块的思路是这样的:
1、通过一个队列来控制并发量
2、如果当前活跃(调用发起但是没有执行完毕)的异步调用量小于限定值,从队列中取出执行
3、如果活跃调用达到限定值,调用暂时存放在队列中
4、每个异步调用结束后,从队列中取出新的异步调用执行
5、当队列的长度大于限制值的2倍或100的时候时候,触发full事件

bagpipe的API主要暴露了一个push方法和full事件:

var Bagpipe = require('bagpipe'); // 设定   发数为10var bagpipe = new Bagpipe(10); for (var i = 0; i < 100; i++) {    bagpipe.push(async, function () {         // 异步回调执行    }); }bagpipe.on('full', function (length) {     console.warn('底层系统处理不能及时完成,队列拥堵,目前队列长度为:' + length);});

bagpipe类似于打开了一道窗口,允许异步调用并行进行,但是严格限定上限。

拒绝模式

如果调用由实时方面的需求,需要快速返回。那么在这种场景下需要快速失败,让调用方法尽早返回,而不要浪费不必要的等待时间,bagpipe为此支持了拒绝模式。

var bagpipe = new Bagpipe(10, {    refuse: true });

在拒绝模式下,如果等待的调用队列也满了以后,新来的调用就直接返回一个队列太忙的异常。

超时控制

造成队列拥塞的主要原因是异步调用耗时太久,调用产生的速度远远大于执行的速度。为了防止某些异步调用使用太多时间,我们需要设置一个时间基线,将那些执行时间太久的异步调用清理出活跃队列,让排队的异步调用尽快执行。

var bagpipe = new Bagpipe(10, {    timeout: 3000 });

bagpipe的GitHub链接:
https://github.com/JacksonTian/bagpipe/blob/master/README_CN.md

async的解决方案

async也提供了一个方法用于异步调用的限制:parallelLimit。

这里写图片描述

原创粉丝点击