文章目录
一、期约
1.同步/异步执行的二元性
Promise的设计很大程度上会导致一种完全不同于JavaScript的计算模式。下面的例子完美地展示了这一点,其中包含了两种模式下抛出错误的情形:
try{
throw new Error('foo');
}catch(e){
console.log(e);// Error: foo
}
try{
Promise.reject(new Error('bar'));
}catch(e){
console.log(e);//Uncaught (in promise) Error: bar
}
第一个try/catch抛出并捕获了错误,第二个try/catch抛出错误却没有捕获到。乍一看这可能有点违反直觉,因为代码中确实是同步创建了一个拒绝的期约实例,而这个实例也抛出了包含拒绝理由的错误。这里的同步代码之所以没有捕获期约抛出的错误(意思就是不能打印,直接抛错),是因为它没有通过异步模式捕获错误。从这里就可以看出期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。
重点:
拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队列来处理的。因此,
try/catch块并不能捕获该错误
。代码一旦开始以异步模式执行,则唯一与之交互的方式就是使用异步结构——更具体地说,就是期约的方法
。
2.期约的实例方法
Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个then()方法接收最多两个参数:
onResolved处理程序和onRejected处理程序(这是两个函数)
。这两个参数都是可选的,如果提供的话,则会在期约分别进入“兑现”和“拒绝”状态时执行。
function onResolved(id){
setTimeout(console.log,0,id,'resolved');
}
function onRejected(id){
setTimeout(console.log,0,id,'rejected');
}
let p1 = new Promise((resolve,reject)=>setTimeout(resolve,3000));
let p2 = new Promise((resolve,reject)=>setTimeout(reject,3000));
p1.then(()=>onResolved('p1'),()=>onRejected('p1'));//p1 resolved
p2.then(()=>onResolved('p2'),()=>onRejected('p2'));//p2 rejected
,
两个处理程序参数都是可选的。而且,传给then()的任何非函数类型的参数都会被静默忽略
。如果想只提供onRejected参数,那就要在onResolved参数的位置上传入undefined。这样有助于避免在内存中创建多余的对象,对期待函数参数的类型系统也是一个交代。
function onResolved(id){
setTimeout(console.log,0,id,'resolved');
}
function onRejected(id){
setTimeout(console.log,0,id,'rejected');
}
let p1 = new Promise((resolve,reject)=>setTimeout(resolve,3000));
let p2 = new Promise((resolve,reject)=>setTimeout(reject,3000));
//非含少数处理程序会被静默忽略,不推荐
p1.then('gobbeltygook');
//不传onResolved 处理程序的规范写法
p2.then(null,()=>onRejected('p2'));//p2 rejected
3.非重入期约方法
当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的。这个特性由JavaScript运行时保证,被称为“非重入”(non-reentrancy)特性。下面的例子演示了这个特性:
//创建解决期约
let p = Promise.resolve();
//添加解决处理程序
//直觉上,这个处理会等到期约一解决就执行
p.then(()=>console.log('onResolved handler'));
//同步输出,证明then()已经返回
console.log('then() returns');
//实际的输出
//then() returns
// onResolved handler
在这个例子中,在一个解决期约上调用then()会把onResolved处理程序推进消息队列。但这个处理程序在当前线程上的同步代码执行完成前不会执行。因此,跟在then()后面的同步代码一定先于处理程序执行。
另外一个方式:
先添加处理程序后解决期约也是一样的。如果添加处理程序后,同步代码才改变期约状态,那么处理程序仍然会基于该状态变化表现出非重入特性。下面的例子展示了即使先添加了onResolved处理程序,再同步调用resolve(),处理程序也不会进入同步线程执行:
let synchronousResolve;
//创建一个期约并将解决函数保存在一个局部变量中
let p = new Promise(()=>{
synchronousResolve = function(){
console.log('1:invoking resolve()');
resolve();
console.log('2:resolve() returns');
};
});
p.then(()=>console.log('4:then() handler executes'));//这个就是处理程序
synchronousResolve();
console.log('3:synchronousResolve() returns');
//实际输出:
// 1:invoking resolve()
// 2:resolve() returns
// 3:synchronousResolve() returns
// 4:then() handler executes
在这个例子中,即使期约状态变化发生在添加处理程序之后,处理程序也会等到运行的消息队列让它出列时才会执行。
这几个方法都是异步方法:
非重入适用于onResolved/onRejected处理程序、catch()处理程序和finally()处理程序。下面的例子演示了这些处理程序都只能异步执行:
let p1 = Promise.resolve();
p1.then(()=>console.log('p1.then() onResolved'));
console.log('p1.then() returns');
let p2 = new Promise();
p2.then(null,()=>console.log('p2.then() onRejected'));
console.log('p2.then() returns');
let p3 = Promise.reject();
p3.catch(()=>console.log('p3.catch() onRejected'));
console.log('p3.then() returns');
let p4 = Promise.resolve();
p4.finally(()=>console.log('p4.finally() onFinally'));
console.log('p4.finally() returns');
// p1.then() returns
// p2.then() returns
// p3.then() returns
// p4.then() returns
// p1.then() onResolved
// p2.then() onRejected
// p3.then() onRejected
// p4.then() onFinally
4.传递解决值和拒绝理由
在执行函数中,解决的值和拒绝的理由是分别作为resolve()和reject()的第一个参数往后传的。然后,这些值又会传给它们各自的处理程序,作为onResolved或onRejected处理程序的唯一参数。下面的例子展示了上述传递过程:
let p1 = new Promise((resolve,reject)=>resolve('foo'));
p1.then((value)=>console.log(value));//foo
let p2 = new Promise((resolve,reject)=>reject('bar'));
p2.catch((reason)=>console.log(reason));//bar
Promise.resolve()和Promise.reject()在被调用时就会接收解决值和拒绝理由。同样地,它们返回的期约也会像执行器一样把这些值传给onResolved或onRejected处理程序:
let p1 = Promise.resolve('foo');
p1.then((value)=>console.log(value));//foo
let p2 = Promise.reject('bar');
p2.catch((reason)=>console.log(reason));//bar
处理错误的时候的情况:
Promise.resolve().then()的错误最后才出现,这是因为它需要在运行时消息队列中添加处理程序;也就是说,在最终抛出未捕获错误之前它还会创建另一个期约。
这个例子同样揭示了异步错误有意思的副作用。正常情况下,在通过throw()关键字抛出错误时,JavaScript运行时的错误处理机制会停止执行抛出错误之后的任何指令:
但是,在期约中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令:
Promise.reject(Error('foo'));
console.log('bar');
//bar
//Uncaugh (in promise) Error:foo
如本章前面的Promise.reject()示例所示,异步错误只能通过异步的onRejected处理程序捕获:
//正确
Promise.reject(Error('foo')).catch((e)=>{});
//不正确
try{
Promise.reject(Error('foo'));
}catch(e){}
这不包括捕获执行函数中的错误,在解决或拒绝期约之前,仍然可以使用try/catch在执行函数中捕获错误:
let p = new Promise((resolve,reject)=>{
try{
throw Error('foo');
}catch(e){}
resolve('bar');
});
setTime(console.log,0,p);//Promise <resolved>:bar
then()和catch()的onRejected处理程序在语义上相当于try/catch。出发点都是捕获错误之后将其隔离,同时不影响正常逻辑执行。为此,onRejected处理程序的任务应该是在捕获异步错误之后返回一个解决的期约。下面的例子中对比了同步错误处理与异步错误处理:
console.log('begin synchronous execution');
try{
throw Error('foo');
}catch(e){
console.log('caught error',e);
}
console.log('continue synchronous execution');
//begin synchronous execution
//caught error Error:foo
//continue synchronuous execution
new Promise((resolve,reject)=>{
console.log("begin asynchronous excution");
reject(Error('bar'));
}).catch((e)=>{
console.log('caught error',e);
}).then(()=>{
console.log('continue aynchronous exectution');
});
//begin asynchronous execution
//caught error Error:bar
//continue asynchronous execution
5. 期约连锁与期约合成
多个期约组合在一起可以构成强大的代码逻辑。这种组合可以通过两种方式实现:期约连锁与期约合成。前者就是一个期约接一个期约地拼接,后者则是将多个期约组合为一个期约。
5.1 期约连锁
把期约逐个地串联起来是一种非常有用的编程模式。之所以可以这样做,是因为每个期约实例的方法(then()、catch()和finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的“期约连锁”。比如:
let p = new Promise((resolve,reject)=>{
console.log('first');
resolve();
});
p.then(()=>console.log('second'))
.then(()=>console.log('third'))
.then(()=>console.log('fourth'));
//first
//second
//third
//fourth
这个实现最终执行了一连串同步任务。正因为如此,这种方式执行的任务没有那么有用,毕竟分别使用4个同步函数也可以做到:
(()=>console.log('first'))();
(()=>console.log('seconed'))();
(()=>console.log('third'))();
(()=>console.log('fourth'))();
要真正执行异步任务,可以改写前面的例子,让每个执行器都返回一个期约实例。这样就可以让每个后续期约都等待之前的期约,也就是串行化异步任务。比如,可以像下面这样让每个期约在一定时间后解决:
let p1 = new Promise((resovle,reject)=>{
console.log('p1 executor');
setTimeout(resolve,1000);
});
p1.then(()=>new Promise((resove,reject)=>{
console.log('p2 executor');
setTimeout(resolve,1000);
}))
.then(()=>new Promise((resolve,reject)=>{
console.log('P3 executor');
setTimeout(resolve,1000);
}))
.then(()=>new Promise((resolve,reject)=>{
console.log('P4 executor');
setTimeout(resolve,1000);
}));
// p1 executor
// p2 executor
// p3 executor
// p4 executor
把生成期约的代码提取到一个工厂函数中,就可以写成这样:
function delayedResolve(str){
return new Promise((resovle,reject)=>{
console.log(str);
setTimeout(resolve,1000);
});
}
delayedResolve('p1 executor')
.then(()=>delayedResovle('p2 executor'))
.then(()=>delayedResolve('p3 executor'))
.then(()=>delayedResolve('p4 executor'))
// p1 executor
// p2 executor
// p3 executor
// p4 executor
每个后续的处理程序都会等待前一个期约解决,然后实例化一个新期约并返回它。这种结构可以简洁地将异步任务串行化,解决之前依赖回调的难题。
,期约的处理程序是按照它们添加的顺序执行的。由于期约的处理程序是先添加到消息队列,然后才逐个执行,因此构成了层序遍历。
5.2 Promise.all()和Promise.race()
Promise类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()和Promise.race()。而合成后期约的行为取决于内部期约的行为。
Promise.all
Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新期约:
let p1 = Promise().all([
Promise.resovle(),
Promise.resovle()
]);
//可迭代对象中的元素会通过Promise.resolve()转换为期约
let p2 = Promise.all([3,4]);
//空的可迭代对象等价于Promise.resolve()
let p3 = Promise.all([]);
//无效的语法
let p4 = Promise.all();
// TypeEroor:cannot read Symbol.iterator of undefined
合成的期约只会在每个包含的期约都解决之后才解决:
如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序:
let p1 = Promise().all([
Promise.resovle(3),
Promise.resovle()
Promise.resolve(4)
]);
p.then((values)=>setTimeout(console.log,0,valuse));//[3,undefined,4]
Promise.race()
Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个方法接收一个可迭代对象,返回一个新期约: