期约状态机 Promise可以通过 new 操作符来实例化。创建新期约时需要传入 执行器(executor)函数作为参数
1 2 3 let p = new Promise (() => {});setTimeout (console .log, 0 , p);
在把一个期约实例传给 console.log()时,控制台输出(可能因浏览器不同而略有差异)表明该实例处于待定(pending)状态。如前所述,期约是一个有状态的对象,可能处于如下 3 种状态之一:
待定 (pending)是期约的最初始状态。在待定状态下,期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态。 无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,期约的状态就不再改变。而有时候也称为“解决”,resolved)
兑现 (fulfilled)每个期约只要状态切换为兑现,就会有一个私有的内部值(value)
拒绝 (rejected)如果期约被拒绝,程序就会期待期约状态改变时可以拿到拒绝的理由
期约用途 期约主要有两大用途。首先是抽象地表示一个异步操作。期约的状态代表期约是否完成。某些情况下,这个状态机就是期约可以提供的最有用的信息。
通过执行函数控制期约状态 期约的状态是私有的,所以只能在内部进行操作。内部操作在期约的执行器函数中完成。执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换 。其中,控制期约状态的转换是 通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。另外,调用 reject()也会抛出错误
1 2 3 4 5 6 let p1=new Promise ((resolve,reject )=> resolve());setTimeout (console .log,0 ,p1);let p2 = new Promise ((resolve, reject ) => reject());setTimeout (console .log, 0 , p2);
在前面的例子中,并没有什么异步操作,因为在初始化期约时,执行器函数已经改变了每个期约的状态,执行器函数是同步执行的。这是因为执行器函数是期约的初始化程序。
添加 setTimeout 可以推迟切换状态:
1 2 3 4 let p = new Promise ((resolve, reject ) => setTimeout (resolve, 1000 ));setTimeout (console .log, 0 , p);
无论 resolve()和 reject()中的哪个被调用,状态转换都不可撤销了。于是继续修改状态会静默失败,如下所示:
1 2 3 4 5 6 let p = new Promise ((resolve, reject ) => {resolve(); reject(); }); setTimeout (console .log, 0 , p);
Promise.resolve() 下面两个期约实例实际上是一样的:
let p1 = new Promise((resolve, reject) => resolve()); let p2 = Promise.resolve();
这个解决的期约的值对应着传给 Promise.resolve()的第一个参数。使用这个静态方法,实际上可以把任何值都转换为一个期约
1 2 3 4 5 6 7 8 setTimeout (console .log,0 ,Promise .resolve());setTimeout (console .log, 0 , Promise .resolve(3 ));setTimeout (console .log, 0 , Promise .resolve(4 , 5 , 6 ));
对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此,Promise.resolve()可以说是一个幂等方法
1 2 3 4 5 6 7 8 9 10 11 let p = Promise .resolve(7 );setTimeout (console .log, 0 , p === Promise .resolve(p));setTimeout (console .log, 0 , p === Promise .resolve(Promise .resolve(p)));let p = new Promise (() => {});setTimeout (console .log, 0 , p); setTimeout (console .log, 0 , Promise .resolve(p)); setTimeout (console .log, 0 , p === Promise .resolve(p));
注意,这个静态方法能够包装任何非期约值,包括错误对象,并将其转换为解决的期约。因此,也可能导致不符合预期的行为:
1 2 3 4 let p = Promise .resolve(new Error ('foo' ));setTimeout (console .log, 0 , p);
Promise.reject() 与 Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能通过 try/catch 捕获,而只能通过拒绝处理程序捕获)。下面的两个期约实例实际上是一样的:
let p1 = new Promise((resolve, reject) => reject()); let p2 = Promise.reject();
这个拒绝的期约的理由就是传给 Promise.reject()的第一个参数。这个参数也会传给后续的拒绝处理程序:
1 2 3 4 let p = Promise .reject(3 );setTimeout (console .log, 0 , p); p.then(null , (e ) => setTimeout (console .log, 0 , e));
关键在于,Promise.reject()并没有照搬 Promise.resolve()的幂等逻辑。如果给它传一个期约对象,则这个期约会成为它返回的拒绝期约的理由:
1 2 3 setTimeout (console .log, 0 , Promise .reject(Promise .resolve()));
同步/异步执行的二元性 1 2 3 4 5 6 7 8 9 10 11 12 try {throw new Error ('foo' );} catch (e) { console .log(e); } try {Promise .reject(new Error ('bar' ));} catch (e) { console .log(e);}
第一个 try/catch 抛出并捕获了错误,第二个 try/catch 抛出错误却没有捕获到,这里的同步代码之所以没有捕获期约抛出的错误,是因为它没有通过异步模式捕获错误。从这里就可以看出期约真正的异步特性:它们是同步对象( 在同步执行模式中使用),但也是异步执行模式 的媒介。 在前面的例子中,拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队列来处理的 。因此,try/catch 块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互 的方式就是使用异步结构——更具体地说,就是期约的方法。
Promise.prototype.then() Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个 then()方法接收最多 两个参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选 的,如果提供的话,则会在期约分别进入“兑现”和“拒绝”状态时执行。传给 then()的任何非函数类型的参数都会被静 默忽略 。如果想只提供 onRejected 参数,那就要在 onResolved 参数的位置上传入 undefined。这 样有助于避免在内存中创建多余的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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' )); p2.then(() => onResolved('p2' ), () => onRejected('p2' ));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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' ); p2.then(null , () => onRejected('p2' ));
Promise.prototype.then()方法返回一个新的期约实例:
1 2 3 4 5 let p1 = new Promise (() => {}); let p2 = p1.then(); setTimeout (console .log, 0 , p1); setTimeout (console .log, 0 , p2); setTimeout (console .log, 0 , p1 === p2);
这个新期约实例基于 onResovled 处理程序的返回值构建。换句话说,该处理程序的返回值会通过 Promise.resolve()包装来生成新期约。如果没有提供这个处理程序,则 Promise.resolve()就会 包装上一个期约解决之后的值。如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回 值 undefined。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 let p1 = Promise .resolve('foo' ); let p2 = p1.then();setTimeout (console .log, 0 , p2); let p3 = p1.then(() => undefined ); let p4 = p1.then(() => {}); let p5 = p1.then(() => Promise .resolve()); setTimeout (console .log, 0 , p3); setTimeout (console .log, 0 , p4); setTimeout (console .log, 0 , p5); let p6 = p1.then(() => 'bar' ); let p7 = p1.then(() => Promise .resolve('bar' )); setTimeout (console .log, 0 , p6); setTimeout (console .log, 0 , p7); let p8 = p1.then(() => new Promise (() => {})); let p9 = p1.then(() => Promise .reject()); setTimeout (console .log, 0 , p8); setTimeout (console .log, 0 , p9); let p10 = p1.then(() => { throw 'baz' ; }); setTimeout (console .log, 0 , p10); ... let p11 = p1.then(() => Error ('qux' )); setTimeout (console .log, 0 , p11);
onRejected 处理程序也与之类似:onRejected 处理程序返回的值也会被 Promise.resolve() 包装 。乍一看这可能有点违反直觉,但是想一想,onRejected 处理程序的任务不就是捕获异步错误吗? 因此,拒绝处理程序在捕获错误后不抛出异常是符合期约的行为,应该返回一个解决期约
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 let p1 = Promise .reject('foo' ); let p2 = p1.then(); setTimeout (console .log, 0 , p2); let p3 = p1.then(null , () => undefined ); let p4 = p1.then(null , () => {}); let p5 = p1.then(null , () => Promise .resolve()); setTimeout (console .log, 0 , p3); setTimeout (console .log, 0 , p4); setTimeout (console .log, 0 , p5); let p6 = p1.then(null , () => 'bar' ); let p7 = p1.then(null , () => Promise .resolve('bar' )); setTimeout (console .log, 0 , p6); setTimeout (console .log, 0 , p7); let p8 = p1.then(null , () => new Promise (() => {})); let p9 = p1.then(null , () => Promise .reject()); setTimeout (console .log, 0 , p8); setTimeout (console .log, 0 , p9); let p10 = p1.then(null , () => { throw 'baz' ; }); setTimeout (console .log, 0 , p10); let p11 = p1.then(null , () => Error ('qux' )); setTimeout (console .log, 0 , p11);
Promise.prototype.catch() Promise.prototype.catch()方法用于给期约添加拒绝处理程序 。这个方法只接收一个参数: onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype. then(null, onRejected)
1 2 3 4 5 6 7 let p = Promise .reject(); let onRejected = function (e ) { setTimeout (console .log, 0 , 'rejected' ); }; p.then(null , onRejected); p.catch(onRejected);
Promise.prototype.catch()返回一个新的期约实例:
1 2 3 4 5 let p1 = new Promise (() => {}); let p2 = p1.catch(); setTimeout (console .log, 0 , p1); setTimeout (console .log, 0 , p2); setTimeout (console .log, 0 , p1 === p2);
Promise.prototype.finally() Promise.prototype.finally()方法用于给期约添加 onFinally 处理程序,这个处理程序在期 约转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出 现冗余代码。但 onFinally 处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用清理代码
1 2 3 4 5 6 7 let p1 = Promise .resolve(); let p2 = Promise .reject(); let onFinally = function ( ) { setTimeout (console .log, 0 , 'Finally!' ) } p1.finally(onFinally); p2.finally(onFinally);
Promise.prototype.finally()方法返回一个新的期约实例:
1 2 3 4 5 let p1 = new Promise (() => {}); let p2 = p1.finally();setTimeout (console .log, 0 , p1); setTimeout (console .log, 0 , p2); setTimeout (console .log, 0 , p1 === p2);
这个新期约实例不同于 then()或 catch()方式返回的实例。因为 onFinally 被设计为一个状态 无关的方法,所以在大多数情况下它将表现为父期约的传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 let p1 = Promise .resolve('foo' ); let p2 = p1.finally(); let p3 = p1.finally(() => undefined ); let p4 = p1.finally(() => {}); let p5 = p1.finally(() => Promise .resolve()); let p6 = p1.finally(() => 'bar' ); let p7 = p1.finally(() => Promise .resolve('bar' )); let p8 = p1.finally(() => Error ('qux' )); setTimeout (console .log, 0 , p2); setTimeout (console .log, 0 , p3); setTimeout (console .log, 0 , p4); setTimeout (console .log, 0 , p5); setTimeout (console .log, 0 , p6); setTimeout (console .log, 0 , p7); setTimeout (console .log, 0 , p8); let p9 = p1.finally(() => new Promise (() => {})); let p10 = p1.finally(() => Promise .reject()); setTimeout (console .log, 0 , p9); setTimeout (console .log, 0 , p10); let p11 = p1.finally(() => { throw 'baz' ; }); setTimeout (console .log, 0 , p11);
非重入期约方法 当期约进入落定状态 时,与该状态相关的处理程序仅仅会被排期 ,而非立即执行。跟在添加这个处 理程序的代码之后的同步代码 一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联 的状态,执行顺序也是这样的。这个特性由 JavaScript 运行时保证,被称为“非重入”(non-reentrancy) 特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let synchronousResolve; let p = new Promise ((resolve ) => { 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' );
在这个例子中,即使期约状态变化发生在添加处理程序之后,处理程序也会等到运行的消息队列让 它出列时才会执行。
传递解决值和拒绝理由 到了落定状态后,期约会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理 程序。拿到返回值后,就可以进一步对这个值进行操作。比如,第一次网络请求返回的 JSON 是发送第 二次请求必需的数据,那么第一次请求返回的值就应该传给 onResolved 处理程序继续处理。当然,失 败的网络请求也应该把 HTTP 状态码传给 onRejected 处理程序。
在执行函数中,解决的值和拒绝的理由是分别作为 resolve()和 reject()的第一个参数往后传 的。然后,这些值又会传给它们各自的处理程序,作为 onResolved 或 onRejected 处理程序的唯一 参数。
1 2 3 4 let p1 = new Promise ((resolve, reject ) => resolve('foo' )); p1.then((value ) => console .log(value)); let p2 = new Promise ((resolve, reject ) => reject('bar' )); p2.catch((reason ) => console .log(reason));
then()和 catch()的 onRejected 处理程序在语义上相当于 try/catch。出发点都是捕获错误之 后将其隔离,同时不影响正常逻辑执行。为此,onRejected 处理程序的任务应该是在捕获异步错误之 后返回一个解决的期约。下面的例子中对比了同步错误处理与异步错误处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 console .log('begin synchronous execution' ); try { throw Error ('foo' ); } catch (e) { console .log('caught error' , e); } console .log('continue synchronous execution' ); new Promise ((resolve, reject ) => { console .log('begin asynchronous execution' ); reject(Error ('bar' )); }).catch((e ) => { console .log('caught error' , e); }).then(() => { console .log('continue asynchronous execution' ); });
期约连锁 每个后续的处理程序都会等待前一个期约解决,然后实例化一个新期约并返回它。这种结构可以简 洁地将异步任务串行化,解决之前依赖回调的难题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function delayedResolve (str ) { return new Promise ((resolve, reject ) => { console .log(str); setTimeout (resolve, 1000 ); }); } delayedResolve('p1 executor' ) .then(() => delayedResolve('p2 executor' )) .then(() => delayedResolve('p3 executor' )) .then(() => delayedResolve('p4 executor' ))
Promise.all() Promise.all
方法用于将多个 Promise 实例,这个静态方法接收一个可迭代对象,将参数转为 Promise 实例,再包装成一个新的 Promise 实例。
合成的期约只会在每个包含的期约都解决之后才解决
如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的 期约也会拒绝
1 2 3 4 5 6 7 let p1=Promise .all([new Promise (()=> {})]);setTimeout (console .log,0 ,p1);let p2=Promise .all([Promise .resolve(),Promise .reject(),Promise .resolve()]);setTimeout (console .log,0 ,p2);
如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let p = Promise .all([ Promise .resolve(3 ), Promise .resolve(), Promise .resolve(4 ) ]); p.then((values ) => setTimeout (console .log, 0 , values)); let p = Promise .all([ Promise .reject(3 ), new Promise ((resolve, reject ) => setTimeout (reject, 1000 )) ]); p.catch((reason ) => setTimeout (console .log, 0 , reason));
Promise.race() Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个 方法接收一个可迭代对象,返回一个新期约
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 let p1 = Promise .race([ Promise .resolve(3 ), new Promise ((resolve, reject ) => setTimeout (reject, 1000 )) ]); setTimeout (console .log, 0 , p1); let p2 = Promise .race([ Promise .reject(4 ), new Promise ((resolve, reject ) => setTimeout (resolve, 1000 )) ]); setTimeout (console .log, 0 , p2); let p3 = Promise .race([ Promise .resolve(5 ), Promise .resolve(6 ), Promise .resolve(7 ) ]); setTimeout (console .log, 0 , p3);
串行期约合成 1 2 3 4 5 6 7 function addTwo (x ) {return x+1 ;}function addThree (x ) {return x+3 ;}function addFive (x ) {return x+5 ;}function addTen (x ) { return [addTwo,addThree,addFive].reduce((promise,fn )=> promise.then(fn),Promise .resolve(x)); } addTen(8 ).then(console .log);
done Promise 对象的回调链,不管以then
方法或catch
方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为 Promise 内部的错误不会冒泡到全局)。因此,我们可以提供一个done
方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 asyncFunc() .then(f1) .catch(r1) .then(f2) .done(); Promise .prototype.done=function (onResolved,onRejected ) { this .then(onResolved,onRejected) .catch(function (reason ) { setTimeout (()=> {throw reason},0 ); }); }
finally finally()f方法用于指定不管Promise对象最后状态如何,都会执行的操作,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
1 2 3 4 5 6 7 8 9 10 11 12 server.listen(0 ) .then(function ( ) { }) .finally(server.stop); Promise .prototype.finally=function (callback ) { let P=this .constructor; return this .then( value => P.resolve(callback()).then(()=> value), reason => P.resolve(callback()).then(()=> {throw reason}) ); }
用途 将图片的加载写成一个Promise,一旦加载完成,Promise的状态发生变化
1 2 3 4 5 6 7 8 9 const preloadImage=function (path ) { return new Promise (function (resolve,reject ) { const image=new Image(); image.onload=resolve; image.onerror=reject; image.src=path; }) }