本文出处(附所有demo):
https://github.com/bellemere/Promise
为什么我们需要Promise
涉及到异步编程的时候,js经常会遇到回调地狱,例如(test1.js):
function load(url, cb){
setTimeout(function(){
console.log(url);
cb()
}, 1000);
}
load('a.com', function(){
load('b.com', function(){
load('c.com', function(){
load('d.com', function(){
load('e.com', function(){
load('f.com', function(){
load('g.com', function(){
//...
})
})
})
})
})
})
})
上面的嵌套里面往往还含有别的逻辑,所以嵌套会更加恐怖。
而Promise则是如下处理(test2.js):
function load2(url){
return new Promise(function(resolve){
setTimeout(function(){
console.log(url);
resolve();
}, 1000)
})
}
load2('a.com')
.then(function(){
return load2('b.com')
})
.then(function(){
return load2('c.com')
})
.then(function(){
return load2('d.com')
})
.then(function(){
return load2('e.com')
})
.then(function(){
return load2('f.com')
})
.then(function(){
return load2('g.com')
})
从代码阅读体验来说,会好很多。
Promise是如何一步步实现的呢?
最近看到了promise库Q的作者的一篇文章,讲述了从0实现Q的大概步骤及原理。
原文出处
https://github.com/kriskowal/q/tree/v1/design
一些名词
pending:promise起始状态
resolved:异步操作取得了结果(不管成功还是失败)
fulfilled:resolved成功的状态(ES6里面的resolve是进入这种状态)
rejected: resolved失败的状态
前言
假设我们写的某个函数并不是马上返回一个值,那如果我们需要对这个值做一些操作,最直接的方式是给函数传入一个回调函数作为参数:
var oneOneSecondLater = function (callback) {
setTimeout(function () {
//假设value是我们异步操作最终取得的值
var value = 1;
callback(value);
}, 1000);
};
通常我们还需要对错误的情况(比如网络通信失败/文件读写失败)也进行处理,所以除了正常的callback,我们往往还需要提供一个errback的回调函数:
var maybeOneOneSecondLater = function (callback, errback) {
setTimeout(function () {
if (Math.random() < .5) {
callback(1);
} else {
errback(new Error("Can't provide one."));
}
}, 1000);
};
把error当成一个参数传递给回调函数有不同的方法,比如可以当成位置参数,或者通过一个哨兵值来区分。不过,这些没有一个实际地模型化抛出错误。try/catch以及错误的目的是把错误处理延迟到程序确实想要处理它的地方。如果他们没有被处理,必须有一些机制隐式地传递这些错误。
Promise
让我们考虑一种更为通用的方式。
原先我们的函数返回一个值或是抛出一个错误,现在我们返回一个对象,这个对象包含了这个函数的最终结果(成功或者失败)。这个对象就是一个promise,它最终一定会定型(也就是resolved)。我们可以在这个对象上调用一个方法来观察到它最终是成功(fulfilled)还是失败(rejected)了。如果promise对象rejected了,衍生的promise对象会因为相同的理由reject。
在上述的设计理念的迭代过程中,我们先把promise模型化成一个拥有then方法的对象,then方法用来注册我们的回调函数(test3.js):
var maybeOneOneSecondLater = function () {
//保存回调函数
var callback;
setTimeout(function () {
var value = 1;
callback(value);
}, 1000);
return {
then: function (_callback) {
callback = _callback;
}
};
};
var m = maybeOneOneSecondLater();
m.then(()=>console.log(1));
m.then(()=>console.log(2));
m.then(()=>console.log(3));
//3
setTimeout(function(){
m.then(()=>console.log(4));
}, 3000);
//不执行
如上,这个设计有2个缺点:
- 最后一次的then调用决定了使用的回调函数,而我们希望注册的每个回调函数在收到结果的时候都能被通知到。
- 如果回调函数在promise对象构建后的一秒后注册,就不会被调用了。
我们先看看ES6里面Promise针对上面两种情况的处理结果(test4.js):
var p = new Promise(function(resolve){
setTimeout(function(){
resolve()
}, 1000)
});
//注册了多个回调函数
p
.then(()=>console.log(1))
.then(()=>console.log(2))
.then(()=>console.log(3))
//1
//2
//3
//在Promise对象构建后再注册回调函数
setTimeout(function(){
p.then(()=>console.log(4))
}, 3000);
//4
我们希望promise对象能够注册任意数量的回调函数,并且不管在超时前或者已经超时都能够注册。我们可以通过让promise拥有2个状态来实现上述需求。
promise开始的时候是未定型的(pending),这时候所有的回调函数都添加到一个观察数组里面。当promise定型了(resolved),数组里的所有回调函数都会被调用。在promise定型后,新注册的回调函数会马上执行。我们通过观察数组是否存在来区分promise的状态(pending/resolved)。
修改代码如下(test5.js):
var maybeOneOneSecondLater = function () {
//pending存在表示unresolved, 不存在则表示resolved
var pending = [], value;
setTimeout(function () {
value = 1