Promise 迷你书
所谓Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise
是一个对象,从它可以获取异步操作的消息。Promise
提供统一的 API
,各种异步操作都可以用同样的方法进行处理。
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。
从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为resolved
(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event
)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
function asyncFunction() {
(1)
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve('Async Hello world');
}, 16);
});
}
(2)
asyncFunction().then(function (value) {
console.log(value); // => 'Async Hello world'
}).catch(function (error) {
console.log(error);
});
- (1)
new Promise
构造器之后,会返回一个promise
对象; - (2)为
promise
对象用设置.then
调用返回值时的回调函数。
创建promise对象
-
new Promise(fn)
返回一个promise
对象 - 在
fn
中指定异步等处理- 处理结果正常的话,调用
resolve
(处理结果值) - 处理结果错误的话,调用
reject
(Error
对象)
- 处理结果正常的话,调用
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest()
req.open('GET', URL, true)
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText)
} else {
reject(new Error(req.responseText))
}
}
req.onerror = function () {
reject(new Error(req.statusText))
}
})
}
var URL = "http://httpbin.org/get"
getURL(URL).then(function onFulfilled(value){
console.log(value)
}).catch(function onRejected(error){
console.log(error)
})
getURL
只有在通过XHR
取得结果状态为200
时才会调用 resolve
- 也就是只有数据取得成功时,而其他情况(取得失败)时则会调用reject
方法。
resolve(req.responseText)
在response
的内容中加入了参数。 resolve
方法的参数并没有特别的规则,基本上把要传给回调函数参数放进去就可以了。 (then
方法可以接收到这个参数值)
- 为
promise
对象添加处理方法主要有以下两种-
promise
对象被resolve
时的处理(onFulfilled
) -
promise
对象被reject
时的处理(onRejected
)
-
静态方法
Promise.resolve
静态方法Promise.resolve(value)
可以认为是 new Promise()
方法的快捷方式。
比如Promise.resolve(42)
; 可以认为是以下代码的语法糖。
new Promise(function(resolve) {
resolve(42)
})
在这段代码中的resolve(42)
; 会让这个promise
对象立即进入确定(即resolved
)状态,并将42
传递给后面then
里所指定的onFulfilled
函数。
方法
Promise.resolve(value)
; 的返回值也是一个promise
对象,所以我们可以像下面那样接着对其返回值进行.then
调用。
Promise.resolve(42).then(function(value) {
console.log(value)
})
Thenable
Promise.resolve
方法另一个作用就是将 thenable 对象转换为promise
对象。
最简单的例子就是 jQuery.ajax(),它的返回值就是thenable的。因为jQuery.ajax()
的返回值是 jqXHR Object 对象,这个对象具有 .then
方法。
$.ajax('/json/comment.json');// => 拥有 `.then` 方法的对象
将thenable对象转换promise对象
var promise = Promise.resolve($.ajax('/json/comment.json')); // => promise对象
promise.then(function(value){
console.log(value);
});
Promise.reject
Promise.reject(error)
是和 Promise.resolve(value)
类似的静态方法,是 new Promise()
方法的快捷方式。
比如 Promise.reject(new Error("出错了"))
就是下面代码的语法糖形式。
new Promise(function(resolve,reject){
reject(new Error("出错了"));
});
这段代码的功能是调用该promise
对象通过then
指定的 onRejected
函数,并将错误(Error
)对象传递给这个 onRejected
函数。
Promise.reject(new Error("BOOM!")).catch(function(error){
console.error(error);
});
同步调用和异步调用同时存在导致的混乱
mixed-onready.js
function onReady(fn) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
fn();
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
mixed-onready.js会根据执行时DOM
是否已经装载完毕来决定是对回调函数进行同步调用还是异步调用。
如果在调用onReady
之前DOM
已经载入的话,对回调函数进行同步调用;如果在调用onReady
之前DOM
还没有载入的话,通过注册 DOMContentLoaded
事件监听器来对回调函数进行异步调用。因此,如果这段代码在源文件中出现的位置不同,在控制台上打印的log
消息顺序也会不同。
async-onready.js
function onReady(fn) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
setTimeout(fn, 0);
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
Effective JavaScript
- 绝对不能对异步回调函数(即使在数据已经就绪)进行同步调用。
- 如果对异步回调函数进行同步调用的话,处理顺序可能会与预期不符,可能带来意料之外的后果。
- 对异步回调函数进行同步调用,还可能导致栈溢出或异常处理错乱等问题。
- 如果想在将来某时刻调用异步回调函数的话,可以使用
setTimeout
等异步API
。
— David Herman
function onReadyPromise() {
return new Promise(function (resolve, reject) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
resolve();
} else {
window.addEventListener('DOMContentLoaded', resolve);
}
});
}
onReadyPromise().then(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
catch
var promise = Promise.reject(new Error("message"));
promise.catch(function (error) {
console.error(error);
});
实际上这和 catch
是ECMAScript的 保留字 (Reserved Word)有关。
在ECMAScript 3
中保留字是不能作为对象的属性名使用的。 而IE8
及以下版本都是基于ECMAScript 3
实现的,因此不能将 catch
作为属性来使用,也就不能编写类似 promise.catch()
的代码,因此就出现了 identifier not found 这种语法错误了。
使用Promise#then
代替Promise#catch
var promise = Promise.reject(new Error("message"));
promise.then(undefined, function (error) {
console.error(error);
});
Promise.all
function getURL(URL) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest()
req.open('GET', URL, true)
req.onload = function() {
if(req.status === 200) {
resolve(req.responseText)
}
}
req.onerror = function() {
reject(new Error(req.statusText))
}
req.send()
})
}
var request = {
comment: function getComment() {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople() {
return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
}
};
function main() {
return Promise.all([request.comment(), request.people()]);
}
// 运行示例
main().then(function (value) {
console.log(value);
}).catch(function(error){
console.log(error);
});
- main中的处理流程显得非常清晰
- Promise.all 接收 promise对象组成的数组作为参数
Promise.all([request.comment(), request.people()]);
在上面的代码中,request.comment()
和 request.people()
会同时开始执行,而且每个promise
的结果(resolve
或reject
时传递的参数值),和传递给 Promise.all
的promise
数组的顺序是一致的。
也就是说,这时候 .then
得到的promise数组的执行结果的顺序是固定的,即[comment, people]
。
main().then(function (results) {
console.log(results); // 按照[comment, people]的顺序
});
如果像下面那样使用一个计时器来计算一下程序执行时间的话,那么就可以非常清楚的知道传递给 Promise.all
(http://liubin.org/promises-book/#Promise.all) 的promise
数组是同时开始执行的。
promise-all-timer.js
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(delay);
}, delay);
});
}
var startDate = Date.now();
// 所有promise变为resolve后程序退出
Promise.all([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
]).then(function (values) {
console.log(Date.now() - startDate + 'ms');
// 約128ms
console.log(values); // [1,32,64,128]
});
timerPromisefy
会每隔一定时间(通过参数指定)之后,返回一个promise
对象,状态为FulFilled
,其状态值为传给 timerPromisefy
的参数。
而传给 Promise.all
的则是由上述promise
组成的数组。
var promises = [
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
];
这时候,每隔1, 32, 64, 128 ms都会有一个promise
发生 resolve
行为。
也就是说,这个promise
对象数组中所有promise
都变为resolve
状态的话,至少需要128ms。实际我们计算一下Promise.all
的执行时间的话,它确实是消耗了128ms的时间。
从上述结果可以看出,传递给 Promise.all
的promise
并不是一个个的顺序执行的,而是同时开始、并行执行的。
Promise.race
接着我们来看看和 Promise.all
类似的对多个promise对象进行处理的 Promise.race
方法。
它的使用方法和Promise.all
一样,接收一个promise
对象数组为参数。
Promise.all
在接收到的所有的对象promise
都变为 FulFilled
或者 Rejected
状态之后才会继续进行后面的处理, 与之相对的是 Promise.race
只要有一个promise
对象进入FulFilled
或者Rejected
状态的话,就会继续进行后面的处理。
像Promise.all
时的例子一样,我们来看一个带计时器的 Promise.race
的使用例子。
promise-race-timer.js
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(delay);
}, delay);
});
}
// 任何一个promise变为resolve或reject 的话程序就停止运行
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128)
]).then(function (value) {
console.log(value); // => 1
});
上面的代码创建了4个promise
对象,这些promise
对象会分别在1ms,32ms,64ms和128ms后变为确定状态,即FulFilled
,并且在第一个变为确定状态的1ms后, .then
注册的回调函数就会被调用,这时候确定状态的promise
对象会调用 resolve(1)
因此传递给 value
的值也是1,控制台上会打印出1来。
下面我们再来看看在第一个promise
对象变为确定(FulFilled
)状态后,它之后的promise
对象是否还在继续运行。
promise-race-other.js
var winnerPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('this is winner');
resolve('this is winner');
}, 4);
});
var loserPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('this is loser');
resolve('this is loser');
}, 1000);
});
// 第一个promise变为resolve后程序停止
Promise.race([winnerPromise, loserPromise]).then(function (value) {
console.log(value); // => 'this is winner'
});
Promise chain
function doubleUp(value) {
return value * 2;
}
function increment(value) {
return value + 1;
}
function output(value) {
console.log(value);// => (1 + 1) * 2
}
var promise = Promise.resolve(1);
promise
.then(increment)
.then(doubleUp)
.then(output)
.catch(function(error){
// promise chain中出现异常的时候会被调用
console.error(error);
});
这段代码的入口函数是 Promise.resolve(1)
; ,整体的promise chain
执行流程如下所示。
Promise.resolve(1)
; 传递 1 给increment
函数,函数increment
对接收的参数进行 +1
操作并返回(通过return)。这时参数变为2,并再次传给doubleUp
函数,最后在函数output
中打印结果。
每次调用then都会返回一个新创建的promise对象
从代码上乍一看,aPromise.then(...).catch(...)
像是针对最初的 aPromise
对象进行了一连串的方法链调用。
然而实际上不管是then
还是catch
方法调用,都返回了一个新的promise
对象。
下面我们就来看看如何确认这两个方法返回的到底是不是新的promise对象。
var aPromise = new Promise(function (resolve) {
resolve(100);
});
var thenPromise = aPromise.then(function (value) {
console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true
then 的错误使用方法
function badAsyncCall() {
var promise = Promise.resolve();
promise.then(function() {
// 任意处理
return newVar;
});
return promise;
}
这种写法有很多问题,首先在 promise.then
中产生的异常不会被外部捕获,此外,也不能得到then
的返回值,即使其有返回值。
由于每次promise.then
调用都会返回一个新创建的promise
对象,因此需要像上述方式2那样,采用promise chain
的方式将调用进行链式化,修改后的代码如下所示。
function anAsyncCall() {
var promise = Promise.resolve();
return promise.then(function() {
// 任意处理
return newVar;
});
}