前言
Node得益于自身的异步非阻塞I/O展现出的优秀性能而大力发展。但是异步I/O也带来相应的一系列问题,回调地狱、递归嵌套。。。被人们所诟病,本文记录一些异步解决方案,使Node的并行I/O处理得到更好的利用
解决方案
主要解决方案有如下几种:
- 发布/订阅模式
- Promise/Deferred模式
- 事件控制库
发布订阅模式
发布订阅模式是设计模式的一种,在JavaScript中可以利用回调的形式,将不变与变化部分得到解耦,只需关心具体的业务过程,而不需要关心中间处理。Javascript发布订阅模式,Node中events
模块原生实现发布订阅模式
const events = require("events");
const fs = require("fs");
const emitter = new events.EventEmitter();
emitter.on("read_foo",function(err,data){
console.log(data);
})
fs.readFile("./profile.jpg",function(err,data){
return emitter.emit("read_foo",err,data);
})
对同一事件超出10个监听器会造成内存泄漏问题,会得到警告,另外,EventEmitter为了处理error事件,发生错误时会检测是否有error事件监听器,如果存在会将错误交给监听器处理。通过util的模块可以轻松继承EventEmitter
const events = require("events");
const util = require("util");
function Events(){
return events.EventEmitter.call(this);
}
util.inherits(Events,events.EventEmitter);
偏函数
偏函数是通过判断执行次数来判断是否执行函数的功能函数
const after = function(times,callback){
let result = {},count = 0;
return function(key,value){
result[key] = value;
if(++count===times){
return callback(result);
}
}
}
根据这个性质,可以利用偏函数利用Node的异步I/O性能处理,先看一个没有利用偏函数的异步处理过程(常见的回调地狱过程)
fs.readdir(path.join(__dirname,".."),function(err,files){
files.forEach(function(filename,index){
fs.readFile(filename,"utf-8",function(err,file){
// TODO
})
})
})
上述代码常见于Node中,常见的完成任务即可的代码,上述代码完全没有利用到Node的优势异步I/O,将其变为串行处理,而Node的优势在于处理如下过程进行并行处理
fs.readdir(path.join(__dirname,".."),function(err,files){})
fs.readFile("./profile.jpg","utf-8",function(err,data){})
fs.readFile("./sun.jpg","utf-8",function(err,data){})
添加偏函数处理
const fs = require("fs");
const path = require("path");
const events = require("events");
const emitter = new events.EventEmitter();
const after = function(times,callback){
let result = {},count = 0;
return function(key,value){
result[key] = value;
if(++count===times){
return callback(result);
}
}
}
const done = after(3,function(res){
console.log(res)
})
emitter.on("done",done);
fs.readdir(path.join(__dirname,".."),function(err,files){
emitter.emit("done","data1",files);
})
fs.readFile("./profile.jpg","utf-8",function(err,data){
emitter.emit("done","data2",data);
})
fs.readFile("./sun.jpg","utf-8",function(err,data){
emitter.emit("done","data3",data);
})
Promise/Deferred模式
偏函数形式适用于调用之间关联性不大的场景,Promise/Deferred适用场景更大,可以实现链式调用,可以使用q库实现,通过npm install q
进行安装
const fs = require("fs");
const Q = require("q")
function read1(){
const deferred = Q.defer();
fs.readFile("./profile.jpg","utf-8",function(err,file){
deferred.resolve(file);
})
return deferred.promise;
}
function read2(){
const deferred = Q.defer();
fs.readFile("./profile.jpg","utf-8",function(err,file){
deferred.resolve(file);
})
return deferred.promise;
}
read1().then(read2).done(value=>console.log(value),err=>console.log(err));
如下是q库的基本实现原理,使用队列保存回调
const util = require("util");
const Emitter = require("events").EventEmitter;
util.inherits(Promise,Emitter);
function Promise(){
// 队列保存回调
this.queue = [];
this.isPromise = true;
}
// Deferred 状态管理,不变部分
function Deferred(){
this.promise = new Promise();
}
Deferred.prototype.resolve = function(obj){
const promise = this.promise;
const handler = promise.handlers.shift();
if(handler&&handler.fullfilled){
// 判断返回值,如果返回值是promise需要指定当前promise为返回值
const ret = handler.fullfilled(obj);
if(ret.isPromise){
ret.queue = promise.queue;
this.promise = ret;
return;
}
}
}
Deferred.prototype.reject = function(obj){
const promise = this.promise;
const handler = promise.handlers.shift();
if(handler&&handler.error){
// 判断返回值,如果返回值是promise需要指定当前promise为返回值
const ret = handler.reject(obj);
if(ret.isPromise){
ret.queue = promise.queue;
this.promise = ret;
return;
}
}
}
// Promise可变部分
Promise.prototype.then = function(fullfilledHandler,errorHandler,progressHandler){
const handler = {};
if(typeof fullfilledHandler === "function"){
// 保存下来
handler.fullfilled = fullfilledHandler
}
if(typeof errorHandler === "function"){
// 错误只监听一次
handler.reject = fullfilledHandler
}
this.queue.push(handler);
// 返回Promise供链式调用
return this;
}
流程控制库
async
串行控制
async.series([
function(callback){
fs.readFile("./profile.jpg","utf-8",callback);
},
function(callback){
fs.readFile("./profile.jpg","utf-8",callback);
}
],function(err,results){
console.log(results)
})
并行控制
当需要使用并行来提醒性能时,可以使用并行控制
async.parallel([
function(callback){
fs.readFile("./profile.jpg","utf-8",callback);
},
function(callback){
fs.readFile("./profile.jpg","utf-8",callback);
}
],function(err,results){
console.log(results)
})
依赖处理
当两次调用间存在依赖关系时,可以使用waterfall
方法
const fs = require("fs");
const async = require("async")
async.waterfall([
function(callback){
fs.readFile("./profile.jpg","utf-8",function(err,file){
callback(err,file);
});
},
function(arg1,callback){
fs.readFile(arg1,"utf-8",function(err,file){
callback(err,file)
});
}
],function(err,results){
console.log(results)
})
step
优势在于只有一个接口,更加轻量级