Node异步编程

Node异步编程

目前的异步编程主要解决方案有:

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

事件发布/订阅模式

Node自身提供了events模块,可以轻松实现事件的发布/订阅

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

侦听器可以很灵活地添加和删除,使得事件和具体处理逻辑之间可以很轻松的关联和解耦
事件发布/订阅模式常常用来解耦业务逻辑,事件发布者无需关注订阅的侦听器如何实现业务逻辑,甚至不用关注有多少个侦听器存在,数据通过消息的方式可以很灵活的进行传递。
下面的HTTP就是典型的应用场景

var req = http.request(options,function(res){
    res.on('data',function(chunk){
        console.log('Body:'+ chunk);
    })
    res.on('end',function(){
        //TODO
    })
})

如果一个事件添加了超过10个侦听器,将会得到一条警告,可以通过调用emmite.setMaxListeners(0)将这个限制去掉

继承events模块

var events = require('events');
function Stream(){
    events.EventEmiiter.call(this);
}
util.inherits(Stream,events.EventEmitter);

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

所谓雪崩问题,就是在高访问量,大并发量的情况下缓存失效的情况,此时大量的请求同时融入数据库中,数据库无法同时承受如此大的查询请求,进而往前影响到网站整体的响应速度
解决方案:

var proxy = new events.EventEmitter();
var status = "ready";  
var seletc = function(callback){
    proxy.once("selected",callback);//为每次请求订阅这个查询时间,推入事件回调函数队列
    if(status === 'ready'){ 
        status = 'pending';//设置状态为进行中以防止引起多次查询操作
        db.select("SQL",function(results){
            proxy.emit("selected",results); //查询操作完成后发布时间
            status = 'ready';//重新定义为已准备状态
        })
    }
}

多异步之间的协作方案

以上情况事件与侦听器的关系都是一对多的,但在异步编程中,也会出现事件与侦听器多对一的情况。
这里以渲染页面所需要的模板读取、数据读取和本地化资源读取为例简要介绍一下

var count = 0 ;
var results = {};
var done = function(key,value){
    result[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)
})

偏函数方案

var after = function(times,callback){
    var count = 0, result = {};
    return function(key,value){
        results[key] = value;
        count++;
        if(count === times){
            callback(results);
        }
    }
}
var done = after(times,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)
})

引入EventProxy模块方案

var proxy = new EventProxy();
proxy.all('template','data','resources',function(template,data,resources){
    //TODO
})
fs.readFile(template_path,'utf8',function(err,template){
    proxy.emit('template',template);
})
db.query(sql,function(err,data){
    proxy.emit('data',data);
})
l10n.get(function(err,resources){
    proxy.emit('resources',resources);
})

Promise/Deferred模式

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

$.get('/api',{
    success:onSuccess,
    err:onError,
    complete:onComplete
})
//需要严谨设置目标

那么是否有一种先执行异步调用,延迟传递处理的方式的?接下来要说的就是针对这种情况的方式:Promise/Deferred模式

Promise/A

Promise/A提议对单个异步操作做出了这样的抽象定义:

  • Promise操作只会处在三种状态的一种:未完成态,完成态和失败态。
  • Promise的状态只会出现从未完成态向完成态或失败态转化,不能逆反,完成态和失败态不能相互转化
  • Promise的状态一旦转化,就不能被更改。

一个Promise对象只要具备then()即可

  • 接受完成态、错误态的回调方法
  • 可选地支持progress事件回调作为第三个方法
  • then()方法只接受function对象,其余对象将被忽略
  • then()方法继续返回Promise对象,以实现链式调用

通过Node的events模块来模拟一个Promise的实现

var Promise = function(){
    EventEmitter.call(this)
}
util.inherits(Promise,EventEmitter);

Promise.prototype.then = function(fulfilledHandler,errHandler,progeressHandler){
    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;
}

以上通过then()将回调函数存放起来,接下来就是等待success、error、progress事件被触发,实现这个功能的对象称为Deferred对象,即延迟对象。

var Deferred = function(){
    this.state = 'unfulfilled';
    this.promise = new Promise();
}
Deferred.prototype.resolve = function(obj){  //当异步完成后可将resolve作为回调函数,触发相关事件
    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)
}

因此,可以对一个典型的响应对象进行封装

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);
})

要完成上面的转换,首先需要对res对象进行封装,对data,end,error等事件进行promisify

var promisify = function(res){
    var deferred = new Deferred(); //创建一个延迟对象来在res的异步完成回调中发布相关事件
    var result = ''; //用来在progress中持续接收数据
    res.on('data',function(chunk){ //res的异步操作,回调中发布事件
        result += chunk;
        deferred.progress(chunk);
    })
    res.on('end',function(){       
        deferred.resolve(result);
    })
    res.on('error',function(err){
        deferred.reject(err);
    });
    return deferred.promise     //返回deferred.promise,让外界不能改变deferred的状态,只能让promise的then()方法去接收外界来侦听相关事件。
}

promisify(res).then(function(){
    //done
},function(err){
    //error
},function(chunk){
    console.log('Body:' + chunk);
})

以上,它将业务中不可变的部分封装在了Deferred中,将可变的部分交给了Promise

Promise中的多异步协作

Deferred.prototype.all = function(promises){
    var count = promises.length; //记录传进的promise的个数
    var that = this; //保存调用all的对象
    var results = [];//存放所有promise完成的结果
    promises.forEach(function(promise,i){//对promises逐个进行调用
        promise.then(function(data){//每个promise成功之后,存放结果到result中,count--,直到所有promise被处理完了,才出发deferred的resolve方法,发布事件,传递结果出去
            count--;
            result[i] = data;
            if(count === 0){
                that.resolve(results);
            }
        },function(err){
            that.reject(err);
        });
    });
    return this.promise; //返回promise来让外界侦听这个deferred发布的事件。
}

var promise1 = readFile('foo.txt','utf-8');//这里的文件读取已经经过promise化
var promise2 = readFile('bar.txt','utf-8');
var deferred = new Deferred();
deferred.all([promise1,promise2]).thne(function(results){//promise1和promise2的then方法在deferred内部的all方法所调用,用于同步所有的promise
    //TODO
},function(err){
    //TODO
})

支持序列执行的Promise

尝试改造一下代码以实现链式调用

var Deferred = function(){
    this.promise  = new Promise()
}

//完成态
Deferred.prototype.resolve = function(obj){
    var promise = this.promise;
    var handler;
    while((handler = promise.queue.shift())){
        if(handler && handler.fulfilled){
            var ret = handler.fulfilled(obj);
            if(ret && ret.isPromise){
                ret.queue = promise.queue;
                this.promise = ret;
                return;
            }
        }
    }
}

//失败态
Deferred.prototype.reject = function(err){
    var promise = this.promise;
    var handler;
    while((handler = promise.queue.shift())){
        if(handler && handler.error){
            var ret = handler.error(err);
            if(ret && ret.isPromise){
                ret.queue = promise.queue;
                this.promise = ret;
                return
            }
        }
    }
}

//生成回调函数
Deferred.prototype.callback = function(){
    var that = this;
    return function(err,file){
        if(err){
            return that.reject(err);
        }
        that.resolve(file)
    }
}

var Promise = function(){
    this.queue = [];  //队列用于存储待执行的回到函数
    this.isPromise = true;
};
Promise.prototype.then = function(fulfilledHandler,errorHandler,progressHandler){
    var handler = {};
    if(typeof fulfilledHandler === 'function'){
        handler.fulfilled = fulfilledHandler;
    }
    if(typeof errorHandler === 'function'){
        handler.error = errorHandler;
    }
    this.queue.push(handler);
    return this;
}

var readFile1 = function(file,encoding){
    var deferred = new Deferred();
    fs.readFile(file,encoding,deferred.callback());
    return deferred.promise;
}
var readFile2 = function(file,encoding){
    var deferred = new Deferred();
    fs.readFile(file,encoding,deferred.callback());
    return deferred.promise;
}

readFile1('file1.txt','utf8').then(function(file1){
    return readFile2(file1.trim(),'utf8')
}).then(function(file2){
    console.log(file2)
})

流程控制库另外进行总结

以上参考《深入浅出node.js》一书

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

推荐阅读更多精彩内容

  • 前言 上一章讲解了node如何通过事件循环实现异步,包括与各种IO多路复用搭配实现的异步IO已经与IO无关的异步A...
    白昔月阅读 9,772评论 0 6
  • 高阶函数:把函数参数作为参数,或作为返回值 偏函数: 将传入参数作为判断或者其他逻辑条件 注意点 异常处理 异步I...
    wmtcore阅读 480评论 0 0
  • 函数式编程 函数式编程是异步编程的基础,在JS中,将函数作为参数,返回值,都是可以的。这为我们使用回调函数打下了很...
    exialym阅读 1,498评论 0 5
  • 本文首发在个人博客:http://muyunyun.cn/posts/7b9fdc87/ 提到 Node.js, ...
    牧云云阅读 1,679评论 0 3
  • 最近在用node写一个小爬虫学习node,但是遇到一个不大不小的坑,就是如何将异步的node程序串行执行。下面就我...
    大雄good阅读 2,241评论 0 1