Promises
让我们辨明一些误解:Promise不是回调的替代品。Promise提供了一种可信的中介机制 —— 也就是,在你的调用代码和将要执行任务的异步代码之间 —— 来管理回调。
另一种考虑Promise的方式是作为一种事件监听器,你可以在它上面注册监听一个通知你任务何时完成的事件。它是一个仅被触发一次的时间,但不管怎样可以被看作是一个事件。
Promise可以被链接在一起,它们可以是一系列顺序的、异步完成的步骤。与all(..)
方法(用经典的术语将,叫“门”)和race(..)
方法(用经典的术语将,叫“闩”)这样的高级抽象一起,promise链可以提供一种异步流程控制的机制。
还有另外一种概念化Promise的方式是,将它看作一个 未来值,一个与时间无关的值的容器。无论底层的值是否是最终值,这种容器都可以被同样地推理。观测一个Promise的解析会在这个值准备好的时候将它抽取出来。换言之,一个Promise被认为是一个同步函数返回值的异步版本。
一个Promise只可能拥有两种解析结果:完成或拒绝,并带有一个可选的信号值。如果一个Promise被完成,这个最终值称为一个完成值。如果它被拒绝,这个最终值称为理由(也就是“拒绝的理由”)。Promise只可能被解析(完成或拒绝)一次。任何其他的完成或拒绝的尝试都会被简单地忽略,一旦一个Promise被解析,它就成为一个不可被改变的值(immutable)。
显然,有几种不同的方式可以来考虑一个Promise是什么。没有一个角度就它自身来说是完全充分的,但是每一个角度都提供了整体的一个方面。这其中的要点是,它们为仅使用回调的异步提供了一个重大的改进,也就是它们提供了顺序、可预测性、以及可信性。
创建与使用 Promises
要构建一个promise实例,可以使用Promise(..)
构造器:
var p = new Promise( function pr(resolve,reject){
// ..
} );
Promise(..)
构造器接收一个单独的函数(pr(..)
),它被立即调用并以参数值的形式收到两个控制函数,通常被命名为resolve(..)
和reject(..)
。它们被这样使用:
- 如果你调用
reject(..)
,promise就会被拒绝,而且如果有任何值被传入reject(..)
,它就会被设置为拒绝的理由。 - 如果你不使用参数值,或任何非promise值调用
resolve(..)
,promise就会被完成。 - 如果你调用
resolve(..)
并传入另一个promise,这个promise就会简单地采用 —— 要么立即要么最终地 —— 这个被传入的promise的状态(不是完成就是拒绝)。
这里是你通常如何使用一个promise来重构一个依赖于回调的函数调用。假定你始于使用一个ajax(..)
工具,它期预期要调用一个错误优先风格的回调:
function ajax(url,cb) {
// 发起请求,最终调用 `cb(..)`
}
// ..
ajax( "http://some.url.1", function handler(err,contents){
if (err) {
// 处理ajax错误
}
else {
// 处理成功的`contents`
}
} );
你可以将它转换为:
function ajax(url) {
return new Promise( function pr(resolve,reject){
// 发起请求,最终不是调用 `resolve(..)` 就是调用 `reject(..)`
} );
}
// ..
ajax( "http://some.url.1" )
.then(
function fulfilled(contents){
// 处理成功的 `contents`
},
function rejected(reason){
// 处理ajax的错误reason
}
);
Promise拥有一个方法then(..)
,它接收一个或两个回调函数。第一个函数(如果存在的话)被看作是promise被成功地完成时要调用的处理器。第二个函数(如果存在的话)被看作是promise被明确拒绝时,或者任何错误/异常在解析的过程中被捕捉到时要调用的处理器。
如果这两个参数值之一被省略或者不是一个合法的函数 —— 通常你会用null
来代替 —— 那么一个占位用的默认等价物就会被使用。默认的成功回调将传递它的完成值,而默认的错误回调将传播它的拒绝理由。
调用then(null,handleRejection)
的缩写是catch(handleRejection)
。
then(..)
和catch(..)
两者都自动地构建并返回另一个promise实例,它被链接在原本的promise上,接收原本的promise的解析结果 —— (实际被调用的)完成或拒绝处理器返回的任何值。考虑如下代码:
ajax( "http://some.url.1" )
.then(
function fulfilled(contents){
return contents.toUpperCase();
},
function rejected(reason){
return "DEFAULT VALUE";
}
)
.then( function fulfilled(data){
// 处理来自于原本的promise的处理器中的数据
} );
在这个代码段中,我们要么从fulfilled(..)
返回一个立即值,要么从rejected(..)
返回一个立即值,然后在下一个事件周期中这个立即值被第二个then(..)
的fulfilled(..)
接收。如果我们返回一个新的promise,那么这个新promise就会作为解析结果被纳入与采用:
ajax( "http://some.url.1" )
.then(
function fulfilled(contents){
return ajax(
"http://some.url.2?v=" + contents
);
},
function rejected(reason){
return ajax(
"http://backup.url.3?err=" + reason
);
}
)
.then( function fulfilled(contents){
// `contents` 来自于任意一个后续的 `ajax(..)` 调用
} );
要注意的是,在第一个fulfilled(..)
中的一个异常(或者promise拒绝)将 不会 导致第一个rejected(..)
被调用,因为这个处理仅会应答第一个原始的promise的解析。取代它的是,第二个then(..)
调用所针对的第二个promise,将会收到这个拒绝。
在上面的代码段中,我们没有监听这个拒绝,这意味着它会为了未来的观察而被静静地保持下来。如果你永远不通过调用then(..)
或catch(..)
来观察它,那么它将会成为未处理的。有些浏览器的开发者控制台可能会探测到这些未处理的拒绝并报告它们,但是这不是有可靠保证的;你应当总是观察promise拒绝。
注意: 这只是Promise理论和行为的简要概览。要进行更加深入的探索,参见本系列的 异步与性能 的第三章。
Thenables
Promise是Promise(..)
构造器的纯粹实例。然而,还存在称为 thenable 的类promise对象,它通常可以与Promise机制协作。
任何带有then(..)
函数的对象(或函数)都被认为是一个thenable。任何Promise机制可以接受与采用一个纯粹的promise的状态的地方,都可以处理一个thenable。
Thenable基本上是一个一般化的标签,标识着任何由除了Promise(..)
构造器之外的其他系统创建的类promise值。从这个角度上讲,一个thenable没有一个纯粹的Promise那么可信。例如,考虑这个行为异常的thenable:
var th = {
then: function thener( fulfilled ) {
// 永远会每100ms调用一次`fulfilled(..)`
setInterval( fulfilled, 100 );
}
};
如果你收到这个thenable并使用th.then(..)
将它链接,你可能会惊讶地发现你的完成处理器被反复地调用,而普通的Promise本应该仅仅被解析一次。
一般来说,如果你从某些其他系统收到一个声称是promise或thenable的东西,你不应当盲目地相信它。在下一节中,我们将会看到一个ES6 Promise的工具,它可以帮助解决信任的问题。
但是为了进一步理解这个问题的危险,让我们考虑一下,在 任何 一段代码中的 任何 对象,只要曾经被定义为拥有一个称为then(..)
的方法就都潜在地会被误认为是一个thenable —— 当然,如果和Promise一起使用的话 —— 无论这个东西是否有意与Promise风格的异步编码有一丝关联。
在ES6之前,对于称为then(..)
的方法从来没有任何特别的保留措施,正如你能想象的那样,在Promise出现在雷达屏幕上之前就至少有那么几种情况,它已经被选择为方法的名称了。最有可能用错thenable的情况就是使用then(..)
的异步库不是严格兼容Promise的 —— 在市面上有好几种。
这份重担将由你来肩负:防止那些将被误认为一个thenable的值被直接用于Promise机制。
Promise
API
Promise
API还为处理Promise提供了一些静态方法。
Promise.resolve(..)
创建一个被解析为传入的值的promise。让我们将它的工作方式与更手动的方法比较一下:
var p1 = Promise.resolve( 42 );
var p2 = new Promise( function pr(resolve){
resolve( 42 );
} );
p1
和p2
将拥有完全相同的行为。使用一个promise进行解析也一样:
var theP = ajax( .. );
var p1 = Promise.resolve( theP );
var p2 = new Promise( function pr(resolve){
resolve( theP );
} );
提示: Promise.resolve(..)
就是前一节提出的thenable信任问题的解决方案。任何你还不确定是一个可信promise的值 —— 它甚至可能是一个立即值 —— 都可以通过传入Promise.resolve(..)
来进行规范化。如果这个值已经是一个可识别的promise或thenable,它的状态/解析结果将简单地被采用,将错误行为与你隔绝开。如果相反它是一个立即值,那么它将会被“包装”进一个纯粹的promise,以此将它的行为规范化为异步的。
Promise.reject(..)
创建一个立即被拒绝的promise,与它的Promise(..)
构造器对等品一样:
var p1 = Promise.reject( "Oops" );
var p2 = new Promise( function pr(resolve,reject){
reject( "Oops" );
} );
虽然resolve(..)
和Promise.resolve(..)
可以接收一个promise并采用它的状态/解析结果,但是reject(..)
和Promise.reject(..)
不会区分它们收到什么样的值。所以,如果你使用一个promise或thenable进行拒绝,这个promise/thenable本身将会被设置为拒绝的理由,而不是它底层的值。
Promise.all([ .. ])
接收一个或多个值(例如,立即值,promise,thenable)的数组。它返回一个promise,这个promise会在所有的值完成时完成,或者在这些值中第一个被拒绝的值出现时被立即拒绝。
使用这些值/promises:
var p1 = Promise.resolve( 42 );
var p2 = new Promise( function pr(resolve){
setTimeout( function(){
resolve( 43 );
}, 100 );
} );
var v3 = 44;
var p4 = new Promise( function pr(resolve,reject){
setTimeout( function(){
reject( "Oops" );
}, 10 );
} );
让我们考虑一下使用这些值的组合,Promise.all([ .. ])
如何工作:
Promise.all( [p1,p2,v3] )
.then( function fulfilled(vals){
console.log( vals ); // [42,43,44]
} );
Promise.all( [p1,p2,v3,p4] )
.then(
function fulfilled(vals){
// 永远不会跑到这里
},
function rejected(reason){
console.log( reason ); // Oops
}
);
Promise.all([ .. ])
等待所有的值完成(或第一个拒绝),而Promise.race([ .. ])
仅会等待第一个完成或拒绝。考虑如下代码:
// 注意:为了避免时间的问题误导你,
// 重建所有的测试值!
Promise.race( [p2,p1,v3] )
.then( function fulfilled(val){
console.log( val ); // 42
} );
Promise.race( [p2,p4] )
.then(
function fulfilled(val){
// 永远不会跑到这里
},
function rejected(reason){
console.log( reason ); // Oops
}
);
警告: 虽然 Promise.all([])
将会立即完成(没有任何值),但是 Promise.race([])
将会被永远挂起。这是一个奇怪的不一致,我建议你应当永远不要使用空数组调用这些方法。