第十一章:期约
介绍Promise
之前,先介绍一下异步编程
;
- 同步行为:内存中严格按照顺序执行处理器指令。在执行的每一步,都可以推断出程序的状态。
- 异步行为:类似于系统中断,即:当前进程外部的实体可以执行的代码;
11.1 回调地狱
在早期JavaScript
中,只支持定义回调函数
来表明异步;通常需要深度嵌套的回调函数(回调地狱)来解决;
function double(value){
setTimeout( () => setTimeout(console.log, 0, value * 2), 1000);
}
double(3); // 大概1000毫秒后,6
-
异步操作值
-
失败处理
-
嵌套异步回调
11.2 期约
ES6新增了Promise类型,成为了主导性的异步编程机制。如fetch()
和Battery Status API
等浏览器API都是以期约为基础;
11.2.1 期约基础
-
期约需要new操作符来实例化;
-
创建期约的时候,需要传入执行器(不然会报错);
let p = new Promise( ()=>{} ); setTimeout(console.log, 0, p); // Promise { <pending> }
-
期约状态机
pending
:期约的最初始状态——待定;表示尚未开始或正在执行中。resolved
:期约可以落定,落定成功即为解决(resolved
),有时候也称兑换(fulfilled
);rejected
:期约可以落定,落定失败即为拒绝(rejected
);
-
解决值与拒绝理由
-
期约的使用例子
-
通过执行函数控制期约状态
-
Promise.resolve()
期约并非一开始就必须处于创建状态;而是可以通过静态方法:
Promise.resolve()
来创建一个已解决的状态的期约;将任何值转化为期约:
如果传入参数也是期约,则操作是幂等的
-
Promise.reject()
与
Promise.reject()
类似,Promise.reject()
会实例化一个拒绝的期约并抛出一个异步错误;这个异步错误不能被try{}catch{}
捕获; -
同步、异步执行的二元性
Promise
很大程度上会导致一种完全不同的JavaScript计算模式:try{ throw new Error('This is a sync error!'); }catch(e){ console.log(e); // 可以正常捕获错误 } try{ Promise.reject( new Error('This is a async error!') ); }catch(e){ console.log(e); }
11.2.2 期约的实例方法
期约实例是链接外部同步代码与内部异步代码之间的桥梁;而实例的方法有如下功能:
- 访问异步操作返回值(resolve,reject)
- 处理期约成功和失败的结果(then)
- 连续对期约求值()
- 添加期约终止时才会执行的代码(finally)
-
Promise.prototype.then()
-
Promise.prototype.catch()
-
Promise.prototype.finally()
-
非重入期约方法
- 当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而不是立即执行;
- 跟在这些处理程序后面的同步代码,一定会比处理程序先执行
- 以上称为“非重入”特性
let p = Promise.resolve(); // 创建已解决期约 p.then(()=>{ setTimeout(console.log,0,'resolved'); },null); console.log('this code is after p.then()!'); // 实际上输出: // this code is after p.then()! // resolved
当
p.then()
调用onResolve
处理程序的时候,会将其推进消息队列;当线程上同步代码处理完毕后,才会处理消息队列中的异步代码; -
临近处理程序的执行顺序
-
传递解决值与拒绝理由
当状态落定后,期约会提供解决值或拒绝理由,并作为
resolve()
或reject()
的第一个参数向后传递; -
拒绝期约与拒绝错误处理
拒绝期约代表了一种程序状态,即需要中断或特殊处理;在期约执行器函数或处理程序中抛出错误,则会导致拒绝,对于的错误对象,就是拒绝理由。
11.2.3 期约连锁与期约合成
- 期约连锁:将期约一个一个拼接起来
- 期约合成:将多个期约合并为一个期约
-
期约连锁:
由于期约的实例方法,都会返回一个新的期约实例,所以可以形成链式连锁调用;
let p = new Promise((resolve,reject) => { setTimeout(console.log,0,'first'); resolve(); }); p.then(() => { setTimeout(console.log,0,'second') },null) .then(() => { setTimeout(console.log,0,'third') },null) .then(() => { setTimeout(console.log,0,'fourth') },null) // first // second // third // fourth
显然,这样链式调用也可以使用在其他实例方法上
let p = new Promise((resolve,reject) => { setTimeout(console.log,0,'new'); reject(); }) p.then( ()=> setTimeout(console.log,0,'then') ) .catch( ()=> setTimeout(console.log,0,'catch') ) .finally( ()=> setTimeout(console.log,0,'finally') ) // new // catch // finally
-
期约图:
由上可知,期约可以由多个处理程序,所以就会形成一个有向非循环的图;
let A = new Promise( (resolve,reject) => { console.log('A'); resolve(); } ); let B = A.then( ()=> { console.log("B"); } ); let C = A.then( ()=> { console.log("C"); } ); C.then( () => { console.log("D") } ); C.then( () => { console.log("E") } ); /* A / \ B C / \ D E */
-
期约合成:
11.3 异步函数
异步函数,又被称为“async
/await
”(语法关键字);是ES8规范新增的;
其作用在于:以同步方式写的代码可以异步执行。
11.3.1 async
可以让函数具有异步特征;
-
使用
async
async function show(){ console.log(1); } show(); console.log(2); // 1 // 2
-
异步函数返回值
如果异步函数使用
return
返回值,则该值会被Promise.resolve()
包装为一个期约对象 -
注意:
11.3.2 await
暂停异步代码的执行,等待期约解决;
-
使用
await
async function show(){ console.log(1); let p = new Promise( (resolve,reject) => { setTimeout(resolve,2000,3); }) console.log(await p); }
-
await
的限制- 必须在
async
函数中使用 - 不能在顶级上下文,如:script标签、模块中使用;
- 必须在
11.3.3 理解异步函数执行顺序
await
关键字并非只是等待一个值可用而已;JavaScript
运行时在碰到await
关键字的时候,会记录在哪里暂停执行,等到await
右边的值可用的时候,JavaScript运行时会向消息队列推送一个任务,这个任务会恢复异步函数的执行。
async function show(){
console.log(2);
await null;
conosole.log(4);
}
console.log(1);
show();
console.log(3)
注意:如果await
等待的不是一个立即可用的值,则会发生另外的改变;
async function foo() {
console.log(2);
console.log(await Promise.resolve(8));
console.log(9);
}
async function bar() {
console.log(4);
console.log(await 6);
console.log(7);
}
console.log(1);
foo();
console.log(3);
bar();
console.log(5);
11.3.4 异步函数策略
-
实现
sleep()
async function sleep(delay){ return new Promise( (resolve) => setTimeout(resolve,delay)); } async function test(){ const startTime = Date.now(); await sleep(3000); // 利用await暂停3秒左右 console.log(Date.now() - startTime); } test();
-
利用平行执行
-
串行执行期约
-
栈追踪与内存管理