回调
是解决异步问题最早的一种手段。本质就是在未来某一时间点,执行指定代码。 弊端:
- 不具有顺序性。回调代码容易产生跳跃性,不符合人脑顺序性的逻辑,同时还容易造成地狱回调,加大理解难度。
- 不可信任性。回调函数的控制权反转了,通常在第三方手里,这种控制反导致一系列麻烦的信任问题,比如我们无法控制回调的执行次数,执行时机,同时无法捕获错误。
ajax('/list', function() {
//回调函数,在请求到数据后由第三方工具执行。
})
针对回调设计的一种改进:关注分离
ajax('/list', success, fail)
将回调拆分成 成功的回调和失败的回调,fail通常是可选的,如果没有提供的话,这个错误会被吞掉。
Promise
-
是对于回调产生的控制反转的一种解决方案。
-
这种范式规定了:如果我们不把自己程序的continuation传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么。
-
可以从另外一个角度看待Promise的决议:一种在异步任务中作为两个或更多步骤的流程控制机制,时序上的this-then-that。
针对3的解释: 假定要调用一个函数foo(..)执行某个任务。我们不知道也不关心它的任何细节。这个函数可能立即完成任务,也可能需要一段时间才能完成。 我们只需要知道foo(..)什么时候结束,这样就可以进行下一个任务。换句话说,我们想要通过某种方式在foo(..)完成的时候得到通知,以便可以继续下一步。
-
Promise(链)不仅是一个表达多步异步序列的流程控制,还是一个从一个步骤到下一个步骤传递消息的消息通道。
-
从回调的角度理解Prmise: ==在典型的JavaScript风格中,如果需要侦听某个通知,你可能就会想到事件。因此,可以把对通知的需求重新组织为对foo(..)发出的一个完成事件(completion event,或continuation事件)的侦听。==
特性
- 一旦Promise决议,它就永远保持在这个状态。此时它就成为了不变值(immutable value),可以根据需求多次查看。
- 通过.then可以返回一个新的promise实例,实现链式调用。
- API then()符合关注分离原则,支持同时传入成功和失败的回调。
理解
Promise对象就是分离的关注点之间一个中立的第三方协商机制。
也就是说Promise对象只负责对异步代码进行监听,一旦执行结束就resolve或reject;而then函数又是负责监听Promise对象,进而执行异步代码的后续部分。
它是异步代码和后续代码中间的第三方,好处就是实现了关注分离,同时又把回调函数的执行权再次反转到自身。
Promise的API
Promise.resolve
-
返回一个成功的Promise决议。
-
==如果向其传递一个非promise或非thenable的值,就会得到一个用这个值填充的promise。==
const p1 = new Promise((resolve) => {
resolve(20)
})
const p2 = Promise.resolve(20)
p1 == p2 // 这里p1和p2等价
- 而如果向Promise.resolve传递一个Promise对象,就会返回同一个promise,如下:
const p1 = Promise.resolve(20)
const p2 = Promise.resolve(p1)
p1 == p2 // true
补充: 如果向Promise.resolve传递的Promise对象展开后得到一个拒绝状态,那么从Promise.resolve(..)返回的Promise实际上就是这同一个拒绝状态。 ==所以对这个API方法来说,Promise.resolve(..)是一个精确的好名字,因为它实际上的结果可能是完成或拒绝==
- Promise.reolve还有一个用处,可以用来将不可信任的thenable转变成可信任的Promise对象。
Promise.resolve(..)可以接受任何thenable,将其解封为它的非thenable值。从Promise. resolve(..)得到的是一个真正的Promise,是一个可以信任的值。如果你传入的已经是真正的Promise,那么你得到的就是它本身,所以通过Promise.resolve(..)过滤来获得可信任性完全没有坏处。
// p是一个不可信任的thenable值
const p = {
then: function(cb, errcb) {
cb(20),
errcb('error message')
}
}
// 也就意味着:成功和失败的回调都会执行,不可被信任
p.then((res) => {
console.log(res) // 20
}, (err) => {
console.log(err) // error message
})
// 解决办法如下:通过Promise.resolve将p从不可信任的thenable转换成可信任的promise实例
Promise.resolve(p)
.then((res) => {
console.log(res) // 20
}, (err) => {
console.log(err) // 永远不会执行到这
})
使用: 假设我们要调用一个工具foo(..),且并不确定得到的返回值是否是一个可信任的行为良好的Promise,但我们可以知道它至少是一个thenable。Promise.resolve(..)提供了可信任的Promise封装工具。
new Promise()构造器
- 构造器Promise(..)必须和new一起使用,并且必须提供一个函数回调。==这个回调是同步的或立即调用的==。
- reject(..)就是拒绝这个promise;但resolve(..)既可能完成promise,也可能拒绝,要根据传入参数而定。如果传给resolve(..)的是一个非Promise、非thenable的立即值,这个promise就会用这个值完成。==但是,如果传给resolve(..)的是一个真正的Promise或thenable值,这个值就会被递归展开,并且(要构造的)promise将取用其最终决议值或状态。==
then(...)和catch(...)
- Promise决议之后,立即会调用这两个处理函数之一,但不会两个都调用,而且总是异步调用.
- then(..)接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。如果两者中的任何一个被省略或者作为非函数值传入的话,就会替换为相应的默认回调。默认完成回调只是把消息传递下去,而默认拒绝回调则只是重新抛出(传播)其接收到的出错原因。
- catch(..)只接受一个拒绝回调作为参数,并自动替换默认完成回调。换句话说,它等价于then(null, ..)。
- ==如果完成或拒绝回调中抛出异常,返回的promise是被拒绝的。如果任意一个回调返回非Promise、非thenable的立即值,这个值会被用作返回promise的完成值。如果完成处理函数返回一个promise或thenable,那么这个值会被展开,并作为返回promise的决议值。==
Promise链式调用的总结
让我们来简单总结一下使链式流程控制可行的Promise固有特性。
- 调用Promise的then(..)会自动创建一个新的Promise从调用返回。
- 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise就相应地决议。
- 如果完成或拒绝处理函数返回一个Promise,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前then(..)返回的链接Promise的决议值。
一个有趣的案例(用来理解Promise链的错误处理)
默认拒绝处理函数只是把错误重新抛出,这最终会使得p2(链接的promise)用同样的错误理由拒绝。从本质上说,这使得错误可以继续沿着Promise链传播下去,直到遇到显式定义的拒绝处理函数。代码如下:
var p = new Promise( function(resolve, reject){
reject( "Oops" );
} );
var p2 = p.then(
function fulfilled(){
// 永远不会达到这里
}
// 假定的拒绝处理函数,如果省略或者传入任何非函数值
// function(err) {
// throw err;
// }
);
Promise的错误处理
默认情况下,它假定你想要Promise状态吞掉所有的错误。如果你忘了查看这个状态,这个错误就会默默地(通常是绝望地)在暗处凋零死掉。
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// 数字没有string函数,所以会抛出错误,但此处的错误会被吞掉。
console.log( msg.toLowerCase() );
},
function rejected(err){
// 永远不会到达这里
}
);
为了避免丢失被忽略和抛弃的Promise错误,一些开发者表示,Promise链的一个最佳实践就是最后总以一个catch(..)结束,比如:
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// 数字没有string函数,所以会抛出错误,但此处的错误会被吞掉。
console.log( msg.toLowerCase() );
},
function rejected(err){
// 永远不会到达这里
}
).catch( handleErrors );
Promise模式
Promise主要用来处理异步流程控制,但同时也有几个变体,使得代码编写更高效,可以使用它们用作构建其他模块的基本块。
当心!若向Promise.all([ .. ])传入空数组,它会立即完成,但Promise. race([ .. ])会挂住,且永远不会决议
Promise.all
- 严格说来,传给Promise.all([..])的数组中的值可以是Promise、thenable,甚至是立即值。就本质而言,列表中的每个值都会通过Promise. resolve(..)过滤,以确保要等待的是一个真正的Promise,所以立即值会被规范化为为这个值构建的Promise。如果数组是空的,主Promise就会立即完成。
- Promise.all([ .. ])需要一个参数,是一个数组,通常由Promise实例组成。从Promise. all([ .. ])调用返回的promise会收到一个完成消息(代码片段中的msg)。这是一个由所有传入promise的完成消息组成的数组,与指定的顺序一致(与完成顺序无关)。
- 从Promise.all([ .. ])返回的主promise在且仅在所有的成员promise都完成后才会完成。如果这些promise中有任何一个被拒绝的话,主Promise.all([ .. ])promise就会立即被拒绝,并丢弃来自其他所有promise的全部结果。
Promise.race
- 与Promise.all([..])类似,一旦有任何一个Promise决议为完成,Promise.race([ .. ])就会完成;一旦有任何一个Promise决议为拒绝,它就会拒绝。
- 一项竞赛需要至少一个“参赛者”。所以,如果你传入了一个空数组,主race([..]) Promise永远不会决议,而不是立即决议。这很容易搬起石头砸自己的脚!ES6应该指定它完成或拒绝,抑或只是抛出某种同步错误。遗憾的是,因为Promise库在时间上早于ES6 Promise,它们不得已遗留了这个问题,所以,要注意,永远不要递送空数组。