nodejs
编程经验都会遇到回调函数嵌套的问题,就是大量的回调函数慢慢向右侧屏幕延伸的一种状态。解决此类问题一般有三种做法:1、用事件函数的订阅/通知机制, 把每一层嵌套拆分成多个事件监听。2、利用一些现成的异步函数库来解决这个问题,例如:async
, step
都属于这类函数库。3、拥抱promise
。传送门: promise-spec。promise
概念已经被提出很久了,前端工程师们大致都或多或少有听说过promise
的概念吧,在Firefox和Chrome这样技术比较超前的浏览器上,不需要安装额外的插件就能使用Promise功能。 promise
类库的实现基于标准的Promises/A+
基本规范。常用的类库有:Q
when
等。通过 new Promise 实例化的对象有以下三个状态。
1. resolve, 成功状态, 此时会执行注入的onFulfilled
函数
2. reject, 失败状态, 此时会执行注入的onRejected
函数
3. pending,promise
刚创建后的初始化状态
假设要读取两个文件,当两个文件都读取完毕之后合并文件数据,传递给调用者。
传统的回调形式:
var fs = require('fs');
function readFiles(file1, file2, onDone) {
fs.readFile(file1, 'utf-8', function(error, data1) {
fs.readFile(file2, 'utf-8', function(error, data2) {
onDone(data1 + data2);
});
});
}
readFiles('./file1.txt', './file2.txt', function(data) {
console.log(data);
});
这大概是最初级的解决方案,两个互不依赖的异步操作,大可不必串行。
来看看利用事件订阅/发布的方式应该怎么写:
var util = require('util');
var events = require('events');
function TestEventEmitter() {
events.EventEmitter.call(this);
}
util.inherits(TestEventEmitter, events.EventEmitter);
TestEventEmitter.prototype.all = function() {
if(arguments.length < 2) {
throw new TypeError('all must a latest two params');
}
var params = Array.prototype.slice.call(arguments),
handler = params.splice(-1, 1)[0],
count = params.length,
result = [],
self = this;
for(var i = 0; i < count; i++) {
this.once(params[i], function(ret) {
result.push(ret);
if(result.length === count) {
handler.apply(self, result);
}
});
}
};
var ev = new TestEventEmitter();
ev.all('readFile1', 'readFile2', function(data1, data2) {
console.log('result' + data1 + data2);
});
function readFile(fileName, encoding, onDone) {
fs.readFile(fileName, encoding, onDone);
}
readFile('file1.txt', 'utf-8', function(err, data) {
ev.emit('readFile1', data);
});
readFile('file2.txt', 'utf-8', function(err, data) {
ev.emit('readFile2', data);
});
事件订阅发布的形式一个优点是代码比较直观,缺点是必须把每个操作都封装成一个事件发送的操作。如果要解决的问题不是太复杂可以考虑这种方式。
考虑到要读取的两个问题没有相互依赖用promise可以写成如下形式:
var Promise = require('./promise-new');
function readFilePromise(fileName, encoding) {
return new Promise(function(onResolve, onReject) {
fs.readFile(fileName, encoding, function(err, data) {
if(err) {
onReject(err);
}
else {
onResolve(data);
}
});
});
}
Promise.all([readFilePromise('file1.txt', 'utf-8'), readFilePromise('file2.txt', 'utf-8')])
.then(function(data) {
console.log('result:' + data);
})
.catch(function(err) {
console.log('err' + err);
});
promise
以相对优雅的方式解决了多个异步并行问题,同时有好的异常捕获机制使得我们再也不用担心异常抛出了,只需再末尾catch
一个异常处理函数即可。
对于不同的场景编写promise
代码,我们必须把需要执行的具体操作封装为一个与promise
结合的异步函数,如上面的readFilePromise
。把该函数转换一下,改成Deferred
的形式可以写成:
var Promise = require('./promise-new');
function Deferred() {
var handler = {};
handler.promise = new Promise(function(resolve, reject) {
handler.resolve = resolve;
handler.reject = reject;
});
return handler;
}
function readFilePromise(fileName, encoding) {
var defer = Deferred();
fs.readFile(fileName, encoding, function(err, data) {
if(err) {
defer.reject(err)
}
else {
defer.resolve(data);
}
});
return defer.promise;
}
对于
fs.readFile
的回调函数处理形式也可以把它抽象出来。Q
里面有一个专门解决这个问题的函数:
var Promise = require('./promise-new');
function Deferred() {
if(!(this instanceof Deferred)) {
return new Deferred();
}
var self = this;
this.promise = new Promise(function(resolve, reject) {
self.resolve = resolve;
self.reject = reject;
});
}
Deferred.prototype.makResolver = function() {
var self = this;
return function(err, data) {
if(err) {
self.reject(err);
}
else if(arguments.length > 2) {
self.resolve([].slice.call(arguments, 1));
}
else {
self.resolve(data);
}
};
}
function readFilePromise(fileName, encoding) {
var defer = Deferred();
fs.readFile(fileName, encoding, defer.makResolver());
return defer.promise;
}
Promise.all([readFilePromise('file1.txt', 'utf-8'), readFilePromise('file2.txt', 'utf-8')])
.then(function(data) {
console.log('result:' + data);
})
.catch(function(err) {
console.log('err' + err);
});
composing promises ,是 promises 的强大能力之一。每一个函数只会在前一个promise 被调用并且完成回调后调用,并且这个函数会被前一个 promise 的输出调用。
假设现在读取三个异步操作,而前一个输出结果被后一个所使用:
readFilePromise('file1.txt', 'utf-8')
.then(function(data1) {
console.log('data1 ' + data1);
return readFilePromise('file2.txt', 'utf-8');
})
.then(function(data2) {
console.log('data2' + data2);
return readFilePromise('file3.txt', 'utf-8');
})
.catch(function(err) {
console.log('err' + err.toString());
})
.then(function(data3) {
console.log('data3 ' + data3);
});
每一个
then
操作都会新建一个promise
对象,如果想让后一个异步操作依赖于前一个的结果则必须把promise
对象返回。
如果file1
不存在那么taskB
永远不会被读取
参考资料:
1. https://github.com/promises-aplus/promises-spec
2. https://github.com/azu/promises-book/issues?state=open
3. http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html