什么是Promise
Promise代理了一个可能要在未来才能到达的值[[PromiseValue]]。Promise的一个最重要的特点是,你可以通过then
来指定当[[PromiseValue]]到来时(或到来失败时)调用的handler。
Promise的4种状态
- fulfilled - 成功,[[PromiseValue]]是成功获取到的值
- rejected - 失败,[[PromiseValue]]是失败的原因
- pending - [[PromiseValue]]还没有到达
- settled - [[PromiseValue]]已经有结果(fulfilled或rejected)
创建Promise
方式1:new Promise(executor)
new Promise( /* executor */ function(resolve, reject) { ... } );
传入Promise()的参数叫做executor
,它封装了获取[[PromiseValue]]的过程。
-
在初始化Promise的过程中,executor被Promise内部代码执行,并给executor传入2个参数:resolve函数, reject函数。
MDN文档:The executor function is executed immediately by the Promise implementation, passing resolve and reject functions (the executor is called before the Promise constructor even returns the created object).
-
即使在executor中调用了resolve或reject,也会先执行完当前的executor函数体(不能像
return
一样直接退出函数体)var p1 = new Promise(function (res, rej) { console.log('before res'); res("ok!"); console.log('after res'); }); console.log('after p1 init, p1:', p1); // before res // after res 它被输出说明:即使在executor中调用了resolve或reject,也会先执行完当前的executor函数体,而不像return那样立即退出函数 // after p1 init, p1: Promise { 'ok!' } 它在'before/after res'以后才被输出说明:Promise在初始化过程中就会同步地调用executor(called synchronously)
-
executor的resolve或reject执行以后,Promise的状态就立刻改变(change synchronously)。
var p1 = new Promise(function (res, rej) { setImmediate(function() { res('haha'); console.log('after res', p1); // executor的res函数执行完毕以后,p1状态已经变为fulfilled }); }); console.log('after init', p1); // 由于executor的res或rej函数还未执行,p1处于pending状态 // after init Promise { <pending> } // after res Promise { 'haha' }
方式2:Promise.resolve(value)
- Promise.resolve(value);
返回一个fulfilled的Promise,[[PromiseValue]]为value。
- Promise.resolve(promise);
直接返回参数promise。
- Promise.resolve(thenable);
将thenable转换为Promise,Promise的状态和[[PromiseValue]]跟随thenable。thenable会在后文讨论。
方式3:Promise.reject(reason)
返回一个rejected的Promise。Promise.reject(reason)
就是下面代码的语法糖形式:
new Promise(function(resolve, reject){
reject(reason);
});
它没有Promise.resolve(something)
这么复杂,不管传入什么,它直接将参数reason作为reject原因,返回一个rejected Promise,即使你闲着没事干传一个Promise进去:
var p = Promise.resolve("res");
Promise.reject(p)
.then((val) => {
console.log('111', val);
}, (err) => {
console.log('222', err === p);
});
// 222 true
为了方便debug,最好传入Error实例。
Promise的核心方法:then(onFulfilled[, onRejected])
先说说then模式
then模式
:你先把成功和失败时要调用的handler传给then函数
,等到时机成熟以后(进入settled状态以后),then函数
就帮你调用合适的那个handler。存在一个这样的then函数
的对象叫做Thenable对象
。
// 一个简单的Thenable对象
var thenable = {
then: function (onFulfilled, onRejected) {
// setTimeout模拟一个需要花2秒的异步过程
setTimeout(function () {
var num = Math.random();
if (num > 0.5) {
onFulfilled(num);
} else {
onRejected(num);
}
}, 2000);
}
}
// 使用方式
thenable.then(
function (result) {
console.log('get result:', result);
},
function (err) {
console.log('get error:', err);
});
then模式类似于我们经常使用的Callback模式。
说回Promise的then方法
Promise的then方法其实就是在普通的then模式的基础上增加了链式调用的功能:then函数
返回Promise对象
,前一个Promise对象
进入settled状态以后才调用下一个then函数
的handler。
Promise.prototype.then()
涉及2个Promise对象:
- 调用then方法的Promise对象,这里用p1表示
- 调用then以后返回的Promise对象,这里用p2表示
p2 = p1.then(onFulfilled, onRejected);
then的作用就是,立即返回一个pending状态的Promise:p2,并在p1进入settled状态以后自动帮你调用handler:
- 如果进入fulfilled状态(成功),自动调用onFulfilled
- 如果进入rejected状态(失败),自动调用onRejected
在调用完handler以后,会根据handler的返回值触发p2的状态改变:
- 如果handler返回一个普通值val,p2状态立即(synchronously)变化:pending-->fulfilled,且p2的[[PromiseValue]]为val。
- 如果handler中throw一个错误err,p2状态立即(synchronously)变化:pending-->rejected,且p2的[[PromiseValue]]为err。
- 如果handler返回一个settled的Promise对象temp,p2状态立即(synchronously)变化:pending-->与temp相同的状态,且p2的[[PromiseValue]]与temp的[[PromiseValue]]相同。
- 如果handler返回一个pending的Promise对象temp,p2的状态不立即改变,而是等到temp进入settled状态以后,p2的状态再(异步地)改变:pending-->与temp相同的状态,且p2的[[PromiseValue]]与temp的[[PromiseValue]]相同。
在这里我们只用关注p2是如何改变的,在后文我会解释p2是什么时候改变的(同步还是异步)以及p2的handler是什么时候调用的。
举个例子:
Promise.resolve('result').then(onFulfilled1, onRejected1).then(onFulfilled2, onRejected2);
这等价于:
var p1 = Promise.resolve('result');
var p2 = p1.then(onFulfilled1, onRejected1);
var p3 = p2.then(onFulfilled2, onRejected2);
- 因为p1是fulfilled状态,所以p1的成功handler——
onFulfilled1
被调用。 - 如果
onFulfilled1
返回普通值(不是Promise),那么p2的状态变化:pending-->fulfilled,且p2的[[PromiseValue]]为onFulfilled1的返回值。接下来p2的成功handler——onFulfilled2
被调用,且传入onFulfilled2
的参数为p2的[[PromiseValue]]。 - 如果
onFulfilled1
返回的是Promise,那么p2的状态和[[PromiseValue]]都跟随这个被返回的Promise。onFulfilled2将在p2 fulfilled以后被调用,onRejected2将在p2 rejected以后被调用,传入的参数都是p2的[[PromiseValue]]。 - 依此类推,p3的状态和[[PromiseValue]]都取决于onFulfilled2/onRejected2的返回值。
handler何时被调用
-
handler的调用异步于then的调用。
A异步于B的意思:A与B在JavaScript消息队列中属于不同的消息。当前消息的调用栈完全退出以后,Event loop再处理下一个消息。Event loop处理完B消息以后可能要再处理0个或多个消息才能处理到A。
- 如果p1是通过
new Promise(executor)
的方式得到,那么除了满足第一条以外,p1 handler的调用还异步于executor中resolve()
、reject()
的调用。 -
如果Promise是由
then
返回的:var p1 = Promise.resolve('haha'); var p2 = p1.then(p1_handler); p2.then(p2_handler)
那么除了满足第一条以外,如我在之前讨论
then
的时候所说:- 如果p1_handler产生同步的结果(包括返回普通值、抛出异常、返回settled Promise),则p2状态立即变化,且p2_handler立即调用。这两者都是同步于p1_handler进行的。
- 如果p1_handler产生异步的结果(返回pending Promise,temp),则p2状态随着temp自动改变,且p2_handler在p2状态改变以后自动调用。这两者不仅异步于p1_handler进行,而且异步于temp的状态改变。
接下来我们一个一个地讨论。
1. handler的调用异步于then的调用
在调用then为p1指定handler以后,并不会立即触发handler的调用,而是向JavaScript消息队列中增加一个消息,然后继续执行then
之后的代码。等到then
所在的执行栈完全弹出,Event loop再处理下一个消息。处理完若干个消息以后,Event loop处理到handler的消息。处理这个消息的时候,先检查p1是否为settled,如果是,则调用对应的handler。
即使p1在调用then时就是settled状态,handler的调用也是异步的。p1在调用then时是pending状态的话就更不用说了。
var p1 = Promise.resolve('haha');
// p1在调用then时就是settled状态
p1.then((val) => {
// 在nextTick以后,p1 handler才调用
console.log('in p1 handler, p1:', p1);
});
console.log('after then called, p1:', p1);
process.nextTick(() => {
console.log('nextTick, p1:', p1);
});
// after then called, p1: Promise { 'haha' }
// nextTick, p1: Promise { 'haha' }
// in p1 handler, p1: Promise { 'haha' }
这也是为什么then返回的p2(在刚被返回的时候)必定处于pending状态。因为p1 handler的调用异步于p1的产生(也就异步于p2的产生)。p2需要等待p1 handler异步调用并返回结果才能改变状态,因此在handler被调用以前,p2都是pending状态:
var p1 = Promise.resolve('haha');
console.log('p1', p1);
var p2 = p1.then((val) => {
console.log('in p1 handler');
return 'xixi';
});
console.log('p2', p2);
setImmediate(() => {
console.log('setImmediate p2', p2);
});
// p1 Promise { 'haha' }
// p2 Promise { <pending> }
// in p1 handler
// setImmediate p2 Promise { 'xixi' }
2. handler的调用异步于executor中resolve()
、reject()
的调用
如果p1是通过new Promise(executor)
的方式得到,那么除了满足第一条以外,p1 handler的调用还异步于executor中resolve()
、reject()
的调用。
也就是说resolve()
、reject()
的调用并不会立即触发handler的调用,而是向JavaScript消息队列中增加一个消息,等待Event loop处理到这个消息。处理这个消息的时候会调用handler。
例子(用node.js运行):
var global_val = 'old value';
var p1 = new Promise((res) => {
setTimeout(function () {
res('haha');
console.log(p1); // p1的状态立刻改变
console.log('immediately', global_val); // 但是此时p1的handler还没有调用
process.nextTick(function () {
console.log('nextTick', global_val); // 此时p1的handler还是没有调用
});
setImmediate(function() {
console.log('setImmediate', global_val); // 此时p1的handler已经调用
});
}, 1000);
});
var p2 = p1.then(() => {
// p1的handler
console.log('in p1 handler');
global_val = 'new value';
});
// Promise { 'haha' }
// immediately old value
// nextTick old value
// in p1 handler
// setImmediate new value
为什么handler要异步于executor的resolve()
、reject()
调用
因为在executor的resolve()
、reject()
的调用以后可能还有其他代码要同步执行(当前handler还没有结束)。前一个handler都还没有执行完,自然不应该开始下一个handler的执行。(handler的执行不应该嵌套,而应该串行)
比如在上面global_val
的例子中,传入setTimeout的函数就是一个handler,调用res('haha')
的时候这个handler还有很多代码要执行。那么下一个handler(p1的handler)不应该打断这些代码的执行。
3. 如果Promise是由then返回的
如果Promise是由then
返回的:
var p1 = Promise.resolve('haha');
var p2 = p1.then(p1_handler);
p2.then(p2_handler)
那么除了满足第一条以外,如我在之前讨论then
的时候所说:
- 如果p1_handler产生同步的结果(包括返回普通值、抛出异常、返回settled Promise),则p2状态立即变化,且p2_handler立即调用。这两者都是同步于p1_handler进行的。
- 如果p1_handler产生异步的结果(返回pending Promise,temp),则p2状态随着temp自动改变,且p2_handler在p2状态改变以后自动调用。这两者不仅异步于p1_handler进行,而且异步于temp的状态改变。
这里给出一个测试代码供大家自行验证,注释中有说明,并且可以通过注释/解注释来修改p1 handler的返回结果。
// 如果p1_handler产生同步的结果(包括返回普通值、抛出异常、返回settled Promise),则p2状态立即变化,且p2_handler立即调用。这两者都是同步于p1_handler进行的。
// 如果p1_handler产生异步的结果(返回pending Promise,temp),则p2状态随着temp自动改变,且p2_handler在p2状态改变以后自动调用。这两者不仅异步于p1_handler进行,而且异步于temp的状态改变。
var p1 = Promise.resolve('haha');
var p2 = p1.then((val) => {
console.log('in p1 handler. p1:', p1, 'p2', p2, 'p3:', p3);
process.nextTick(function () {
// 当p1 handler产生同步的结果时,p2 handler在nextTick之前就被调用,且p2在nextTick时已经settled。说明p2状态改变、p2 handler的调用是同步于p1 handler的。
// 当p1 handler产生异步的结果时,p2 handler在nextTick之后才被调用,且p2在nextTick时依然pending。说明p2状态改变、p2 handler的调用是异步于p1 handler的。
console.log('p1 handler nextTick. p1:', p1, 'p2', p2, 'p3:', p3);
});
// throw 'err!';
// return Promise.resolve('heihei');
// return Promise.reject('heihei');
// return new Promise(res => { // temp Promise
// setTimeout(function () {
// res('heihei');
// process.nextTick(function () {
// // p2在nextTick时依然是pending,说明p2的状态改变异步于temp的状态改变
// console.log('resolve nextTick. p1:', p1, 'p2', p2, 'p3:', p3);
// });
// }, 1000);
// });
return 'heihei';
});
var p3 = p2.then(val => {
console.log('in p2 success handler. p1:', p1, 'p2', p2, 'p3:', p3);
return 'xixi';
}, err => {
console.log('in p2 error handler. p1:', p1, 'p2', p2, 'p3:', p3);
return 'hoho';
});
process.nextTick(function () {
// 它先于'in p1 handler'输出可以说明handler是异步于then调用的
console.log('after then called. p1:', p1, 'p2', p2, 'p3:', p3);
});
异常处理
Promise rejected 后,将跳至带有拒绝回调的下一个 then()(或具有相同功能的 catch())。如果有then(func1, func2)
,则 func1 或 func2 中的一个将被调用,而不可能二者均被调用。但如果是 then(func1).catch(func2)
,则有可能两者均被调用(func1 rejected时)。
asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log("Don't worry about it");
}).then(function() {
console.log("All done!");
})
蓝线表示 fulfilled 的 promise 路径,红路表示 rejected 的 promise 路径。
如果在promise构造函数的回调函数中或then的回调函数中发生了 JavaScript 异常(throw Errow),返回的promise会自动reject。
var jsonPromise = new Promise(function(resolve, reject) {
// JSON.parse throws an error if you feed it some
// invalid JSON, so this implicitly rejects:
resolve(JSON.parse("This ain't JSON"));
});
jsonPromise.then(function(data) {
// This never happens:
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
最佳实践:缓存Promise——重复利用异步操作的结果
var storyPromise;
function getChapter(i) {
storyPromise = storyPromise || getJSON('story.json');
return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
})
}
// and using it is simple:
getChapter(0).then(function(chapter) {
console.log(chapter);
return getChapter(1);
}).then(function(chapter) {
console.log(chapter);
})
假设我们要分别获取story的各个章节(用getChapter函数),每个章节所在的URL存储在story.json
的chapterUrls
数组中。
我们不需要每次要获取章节的时候都先获取一次story.json
在拿到章节所在URL,更好的做法是:调用一次getJSON('story.json')
就将这个Promise缓存到storyPromise
中,将来需要请求story.json
的时候只需要重复利用这个fulfilled的storyPromise
。避免了发送重复的HTTP请求。
Promise一旦settled,[[PromiseValue]]不会再改变,我们可以将它看作一个定值,多次使用。
因为Promise对象无法被外部改变(无论是意外地还是恶意地),我们可以安全地将这个对象交给其他库使用,而不用担心库会修改到Promise的结果。
最佳实践:一个 Promise fulfilled 以后再执行下一个 Promise
除了手动写then回调
来依次执行Promise以外,对于一个数组的任务,我们可以利用array.reduce
的循环来创建then回调
:
// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
// 上一个Promise fulfilled以后才执行下一个getJSON
// 从而保证每一个章节是顺序请求的,从而在页面中是顺序显示的
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve())
参考资料
https://developers.google.com...
https://developer.mozilla.org...
http://liubin.org/promises-book/
Node 定时器详解讲述了process.nextTick()和Promise回调函数(microtask)的执行顺序。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。