Node——异步编程

函数式编程

函数式编程是异步编程的基础,在JS中,将函数作为参数,返回值,都是可以的。这为我们使用回调函数打下了很好的基础。

var points = [40, 100, 1, 5, 25, 10]; 
points.sort(function(a, b) 
{   
    return a - b; 
}); // [ 1, 5, 10, 25, 40, 100 ] 
var isType = function (type) {   
    return function (obj) {     
        return toString.call(obj) == '[object ' + type + ']';   
    }; 
};  
var isString = isType('String'); 
var isFunction = isType('Function'); 

难点

异常处理

由于异步,try/catch这样的代码不会起到任何作用。
所以一般对回调函数都要求第一个参数接收错误信息。
在我们自行编写的异步方法上,也要遵循这个原则,在调用传进来的回调时,第一个参数传异常。
我们的异步方法:

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

函数嵌套过深

这个在各种有回调机制的编程中都是个问题,比如iOS。

多线程编程

由于JS执行在单线程,Node实际上没有充分利用多核CPU的性能。在浏览器端存在同样的问题,因此出现了Web Worker来将于UI无关的计算任务交给其他线程。
Node借鉴了这一点,使用child_process和cluster模块来完成这些。

异步编程解决方案

主要有3种解决方案:

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

事件发布/订阅模式

这个是用途最广的异步方式,操作也很简单,这里我们使用Node自带的events模块。
首先你订阅一个事件,并定义在这个事件发生时你会做什么的回调函数。
然后在事件发生时你触发它。

var emitter = require('events'); 
var myEmitter = new emitter();
myEmitter.on("event1", (message1,message2) => {   
    console.log(message1);   
    console.log(message2);   
}); // 发布 
myEmitter.emit('event1', "I am message1!", "I am message2!");

这是一个很好的解耦逻辑的方式。也是一个很好的封装的方式,只将像暴露的过程和信息通过事件发布的方式告诉外面,外面想使用就只能通过订阅的方式。

继承events
Node中近半数的模块都继承自EventEmitter类,这样方便使用事件。

const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});
myEmitter.emit('event');

或者老一点的办法:

//继承EventEmitter模块,使得自己的类也可以发布事件
var util = require('util');
function Stream() {   
    emitter.EventEmitter.call(this); 
} 
util.inherits(Stream, emitter.EventEmitter); 
var myStream = new Stream();
myStream.on("event1", (message) => {   
    console.log(message);     
}); // 发布 
myStream.emit('event1', "I am stream!");

利用事件队列解决雪崩问题
比如对于一次数据库的查询:

var select = function (callback) {   
    db.select("SQL", function (results) {    
        callback(results);   
    }); 
}; 

这个数据库的查询在没有缓存硬执行时,当请求数量非常大时是对性能影响非常大的,它们查询的是同一个语句,我们只要保证给他们的数据是最新的就行。
这时我们可以使用状态锁:

var status = "ready"; 
var select = function (callback) {   
    if (status === "ready") {     
        status = "pending";     
        db.select("SQL", function (results) {      
            status = "ready";       
            callback(results);     
        });   
    } 
};

但是在这种情况下,连续多次的调用select()时,第前一次完成前,后面的是不会被执行的。
这时加入事件队列就很不错,通过once添加的监听器只能执行一次,在执行后就会将它与关联的事件移除:

var proxy = new events.EventEmitter(); 
var status = "ready"; 
var select = function (callback) {   
    proxy.once("selected", callback);   
    if (status === "ready") {     
        status = "pending";     
        db.select("SQL", function (results) {       
            proxy.emit("selected", results);       
            status = "ready";     
        });   
    } 
}; 

每一次select()被调用时,不管上一次是否执行完,我们都把它压入到一个事件队列中。此时如果上次查询已经执行完了,那么这次查询会立即执行;如果没有执行完,那么这次查询会像上面一样不执行,但是这里不同的是,这个查询的请求没有被忽略,而是在事件队列中等待。
在一个查询执行完并返回结果的时候,这个结果会传给在队列中等待的所有查询请求的回调。这就意味着在这次查询结果产生前进来的所有的查询请求就都得到了结果。
多异步之间的协作方案
有时需要等待多个事件完成才能继续下去,这多个事件很可能没有顺序,没有关联。这时事件与监听器的关系其实是多对一的,最简单直观的办法是使用多层嵌套,一个完成了再执行另一个,可是这样无论从效率还是代码上都是不好的。我们可以使用哨兵变量:

var count = 0; 
var results = {}; 
var done = function (key, value) {   
    results[key] = value;   
    count++;   
    if (count === 3) {    
        render(results);   
    } 
};  
fs.readFile(template_path, "utf8", function (err, template) {  
    done("template", template); 
}); 
db.query(sql, function (err, data) {   
    done("data", data); 
}); 
l10n.get(function (err, resources) {   
    done("resources", resources);  
}); 

这样就可以同步的执行它们了。
如果你想要可定制次数的done函数,这里利用了闭包来保存count的状态:

var after = function (times, callback) {   
    var count = 0, results = {};   
    return function (key, value) {     
        results[key] = value;     
        count++;     
        if (count === times) {       
            callback(results);     
        }   
    }; 
};  
var done = after(times, render);

通过简单的拓展,多对多的形式也可以实现:

var after = function (times, callback) {   
    var count = 0, results = {};   
    return function (key, value) {     
        results[key] = value;     
        count++;     
        if (count === times) {       
            callback(results);     
        }   
    }; 
};  
var done = after(3, render); 
var emitter = new events.Emitter(); 
emitter.on("done", done); 
emitter.on("done", other);  
fs.readFile(template_path, "utf8", function (err, template) {   
    emitter.emit("done", "template", template); 
}); 
db.query(sql, function (err, data) {   
    emitter.emit("done", "data", data); 
}); 
l10n.get(function (err, resources) {   
    emitter.emit("done", "resources", resources); 
});

这样在done事件发生时可以同时触发other回调。

Promise/Deferred模式

这个模式允许你在不完全设置好回调的情况下进行异步调用。使用起来更加灵活。
最后的调用看起来是这样的,我们使用jq来举个例子:

$.when(d)
  .done(function(){ alert("哈哈,成功了!"); })
  .fail(function(){ alert("出错啦!"); });

这样是不是更加方便易读了,而且此时就算你不指定done和fail的回调,d这个函数也会照样完成它的工作。如果你想有多个回调,由于实现了链式调用,继续在后面点就好了。
我们在node里以event为基础来实现它:

//首先promise继承event,我们将在promise里完成事件的订阅和发布
//这里定义了3个方法,done,fail和它们的组合then
//done和fail都使用once来绑定事件,保证其只执行一次
//新定义的这三个方法完成了事件的订阅,并且都返回了自己以完成链式调用
var Promise = function () {   
    emitter.EventEmitter.call(this);
}; 
util.inherits(Promise, emitter.EventEmitter);  
Promise.prototype.then = function (fulfilledHandler, errorHandler, progressHandler) {   
    if (typeof fulfilledHandler === 'function') {       
        this.once('success', fulfilledHandler);   
    }   
    if (typeof errorHandler === 'function') {   
        this.once('error', errorHandler);   
    }   
    if (typeof progressHandler === 'function') {     
        this.on('progress', progressHandler);   
    }   
    return this; 
};
Promise.prototype.done = function (fulfilledHandler) {   
    if (typeof fulfilledHandler === 'function') {       
        this.once('success', fulfilledHandler);   
    }   
    return this; 
};
Promise.prototype.fail = function (errorHandler) {   
    if (typeof errorHandler === 'function') {   
        this.once('error', errorHandler);   
    }   
    return this; 
};

可以看到,这里这三个新的自定义方法的作用就是将各个处理函数存起来,这里整个事件有3个状态:处理中,完成和失败。我们还需要一个对象来触发事件状态的转变:

//deferred这个对象是触发事件状态转变的地方
var Deferred = function () {   
    this.state = 'unfulfilled';  
    this.promise = new Promise(); 
};  
Deferred.prototype.resolve = function (obj) {   
    this.state = 'fulfilled';   
    this.promise.emit('success', obj); 
};  
Deferred.prototype.reject = function (err) {   
    this.state = 'failed';   
    this.promise.emit('error', err); 
};  
Deferred.prototype.progress = function (data) {   
    this.promise.emit('progress', data); 
};

啊,这样我们真正的业务逻辑就可以直接调用这几个方法来触发各个状态了。
为了通用我们可以做一个简单的工具函数:

var when = function (func) {
    var defrred = new Deferred();
    func(defrred);
    return defrred.promise;
};

我们真正的业务逻辑就是func了,我们把defrred对象传给我们的业务逻辑,业务逻辑就可以根据自己的需要来触发事件了。我们这里只返回defrred.promise,不把整个defrred暴露给外面,这样外面就不能随便调用defrred来转变事件的状态了。
我们假设一个业务逻辑:

function eat(dfd) {
    console.log('give me apple or beef');
    var tasks = function(){
        if (food=='apple') {
            console.log('you give me apple');
            dfd.reject();
        } 
        if (food=='beef') {
            console.log('you give me beef');
            dfd.resolve();
        }
    };
    setTimeout(tasks,5000);
    
}

这里使用settimeout来模拟异步。
调用:

when(eat)
    .done(function(){ console.log('I eat beef'); })
    .fail(function(){ console.log('I throw apple'); });

这里就算不给done和fail,eat还是会正常执行,就是会触发没有的事件而已。

多异步协作

这里就又要提到一对多,多对一,多对多。
一对多我们可以直接实现:

when(eat)
    .done(function(){ console.log('I eat beef'); })
    .done(function(){ console.log('I eat beefaaa'); })
    .fail(function(){ console.log('I throw apple'); });

多对一我们就需要新的方法了:

Deferred.prototype.all = function (promises) { 
    var count = promises.length;   
    var that = this;   
    var results = [];   
    promises.forEach(function (promise, i) {     
        promise.then(function (data) {       
            count--;       
            results[i] = data;       
            if (count === 0) {         
                that.resolve(results);       
            }     
        }, function (err) {       
            that.reject(err);     
        });   
    });   
    return this.promise; 
};

在每一个异步事件执行成功检查一下是否所有的都执行完了,都执行完了就调用顶上的全部执行完的方法,有一个fail了就调用顶上的fail方法。
使用after方法封装一下:

var after = function(promises) {
    var defrred = new Deferred();
    return defrred.all(promises);
}

使用:

after([when(eat),when(drink)])
    .done(function(){ console.log('I eat all'); })
    .fail(function(){ console.log('I don\'t like one of those'); })

流程控制库

这种方式并不是规范中主流的方式,但是相应的也更加的灵活。
我们来看看最流行的流程控制模块async。
异步的串行

var async = require("async");
var fs = require("fs");
async.series([function (callback) {  
        console.log("reading 1");   
        fs.readFile('file1.txt', 'utf-8', callback); 
    }, function (callback) {     
        //fs.readFile('file2.txt', 'utf-8', callback);   
        console.log("reading 2");
        callback("2222222","33333");
    }], function (err, results) {   
        // results => [file1.txt, file2.txt] 
        console.log("results:"+results);
    }); 

这个series方法会依次执行数组里的函数,这里的callback是由async通过高阶函数的方式注入,每一个函数里会有callback来接收这个函数要返回的结果,无论是同步的还是异步的,无论执行的快或慢,series都会按照顺序来执行这些函数并将这些结果存在数组中,在所有函数执行完之后,这个数组就可以使用了。
异步的并行执行

async.parallel([   function (callback) {     
        fs.readFile('file1.txt', 'utf-8', callback);   
    },   
    function (callback) {     
        fs.readFile('file2.txt', 'utf-8', callback);   
    } ], 
    function (err, results) {   // results => [file1.txt, file2.txt] 
        console.log("results:"+results);
    });

异步调用的依赖处理
当前一个结果是后一个调用的输入时,问题就不能用series来解决了。

async.waterfall([   function (callback) {     
        //file1.txt里下一个文件的地址
        fs.readFile('file1.txt', 'utf-8', function (err, content) { callback(err, content); });   
    },  function (arg1, callback) {     // arg1 => file2.txt     
        fs.readFile(arg1, 'utf-8', function (err, content) { callback(err, content); }); 
    }], function (err, result) {    
        // result => file2.txt 
        console.log("results:"+result);
    });

自动依赖处理
这里就是async强大的地方了,你给他一个你所有要做的事情的对象,以及每个事情依赖谁,它会自动帮你判断该怎么执行:

var deps = {   
    readConfig: function (callback) {     
        console.log("// read config file");     
        console.log(callback);
    },   
    connectMongoDB: ['readConfig', function (callback) {     
        console.log("// connect to mongodb");     
        console.log(callback);  
    }],   
    connectRedis: ['readConfig', function (callback) {     
        console.log("// connect to redis  ");   
        //callback();
    }],  
    complieAsserts: function (callback) {     
        console.log("// complie asserts");     
        //callback();   
    },   
    uploadAsserts: ['complieAsserts', function (callback) {     
        console.log("// upload to assert");    
        //callback();   
    }],   
    startup: ['connectMongoDB', 'connectRedis', 'uploadAsserts', function (callback) {     
        console.log("// startup ");  
    }] 
};
async.auto(deps); 

其他库
相应的还有其他可用的流程控制库,这里就不一一介绍了,比较有代表的是Step和Wind

异步并发控制

因为是异步的,所以当请求多的时候很可能给系统的低层服务带来很大的压力甚至直接崩溃。比如打开文件这个操作,以前同步的时候同时打开的文件不会太多,都是完成一个再去打开另一个。但是异步时可能一下打开了大量的文件,从而引发问题。
所以我们应该对异步调用的数量做一定的限制。
在async中专门有一个方法来有限制的执行异步调用:

async.parallelLimit([   function (callback) {     
        fs.readFile('file1.txt', 'utf-8', callback);   
    },   function (callback) {     
        fs.readFile('file2.txt', 'utf-8', callback);   
    } ], 1, function (err, results) {   
        console.log("resultsLimit:"+results);
    });
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容

  • 高阶函数:把函数参数作为参数,或作为返回值 偏函数: 将传入参数作为判断或者其他逻辑条件 注意点 异常处理 异步I...
    wmtcore阅读 482评论 0 0
  • Node异步编程 目前的异步编程主要解决方案有: 事件发布/订阅模式 Promise/Deferred模式 流程控...
    俗三疯阅读 611评论 0 2
  • 最近在用node写一个小爬虫学习node,但是遇到一个不大不小的坑,就是如何将异步的node程序串行执行。下面就我...
    大雄good阅读 2,246评论 0 1
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,900评论 25 707
  • 本集匿名投票荣誉榜 诗魁(并列): 子雷,作品《云有禅心逐月来》 时雨,作品《水有恒心滴石穿》 副魁: 小薇,作品...
    霙愔阅读 447评论 2 4