ES6新增了正式的Promise类型,还增加了async和await关键字定义异步函数的机制
11.1 异步编程
异步是为了优化因为计算量大而时间长的操作
11.1.1 同步与异步
同步就是从上往下执行
异步行为类似系统中断
异步操作经常是必要的,因为一直等着一个长时间代码是不可行的
11.1.2 以往的异步编程
以前只支持定义回调函数来表明异步操作完成
串联多个异步操作,会形成回调地狱
异步返回值
回调函数callback可以在异步操作后使用包含异步返回值的代码
function double (value,callback){
setTimeout(()=>callback(value*2),1000)
}
double(3,(x)=>console.log(x))
如果异步返回值又依赖另一个异步返回值,那么回调的情况会变得更加复杂.这种策略是不具有拓展性的,被称为回调地狱
11.2 期约 promise
11.2.2 期约基础
ES6新增的Promise类型,可以通过new操作符实例化
创建Promise时需要传入解释器(executor)作为参数,不然会报错
1.期约状态机
期约是一个有状态的对象
待定pending
兑现fulfilled 也称为解决resolved
拒绝rejected
期约的状态是私有的
2.解决值,拒绝理由以及期约用例
期约的用途:抽象的表示一个异步操作;或是在期约封装的异步操作会生成某个值,而程序期待期约状态改变时可以访问这个值
对此,每个期约只要状态切换为兑现,就会有一个私有的内部值;切换为拒绝时,就会有一个私有的内部理由
3.执行函数控制期约状态
期约的状态是私有的,因此只能在内部进行操作
内部操作在期约的执行器函数中完成
执行器函数有两个功能:初始化期约的异步行为,控制状态的最终转换
控制状态的转换通过调用他的两个函数参数实现,通常命名为resolve()和reject()
调用reject()还会跑出错误
let p1 = new Promise((resolve,reject)=>resolve())
setTimeout(console.log,0,p1)
let p2 = new Promise((resolve,reject)=>reject())
setTimeout(console.log,0,p2)
可以设置一个定时退出功能,避免一直卡在待定状态
let p1 = new Promise((resolve,reject)=>{
setTimeout(reject,1000)
})
4.Promise.resolved()
调用Promise.resolved()可以实例化一个解决的期约
// 等价
let p1 = Promise.resolve()
let p2 = new Promise((resolve,reject)=>resolve())
函数的第一个参数代表解决期约的值。用这个静态方法可以把所有值都转化为一个期约
setTimeout(console.log, 0, Promise.resolve(3))
5.Promise.reject()
和.Promise.resolved()类似,但是传入的第一个参数表示的是拒绝理由
这个方法还会抛出一个错误
setTimeout(console.log, 0, Promise.reject(3))
6.同步/异步执行的二元性
期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介
因为代码一旦以异步模式开始执行,与之交互的方式就是使用异步结构--期约的方法
11.2.3 期约的实例方法
期约的实例方法是连接外部同步代码与内部异步代码的桥梁
1.实现Thenable接口
在ECMAScript暴露的异步接口中,任何对象都有then()函数,可以实现Thenable接口
2.Promise.prototype.then()
为期约实例添加处理程序的主要方法
接受两个参数:onResolved和onRejected 在进入兑现和拒绝状态时执行
// onResloved() 和 onRejected()函数
function onResloved(id){
setTimeout(console.log,0,id,'resloved')
}
function onRejected(id){
setTimeout(console.log,0,id,'rejected')
}
let p1 = new Promise((reslove,reject) => setTimeout(reslove,3000))
let p2 = new Promise((reslove,reject) => setTimeout(reject,3000))
p1.then(()=>onResloved('p1'),()=>onRejected('p1'))
p2.then(()=>onResloved('p2'),()=>onRejected('p2'))
// 3s后
// p1 resloved
// p2 rejected
3.Promise.prototype.catch()
给期约添加拒绝处理程序,实际上就是个语法糖
等价于Promise.prototype.then(null,onRejectd)
4.Promise.prototype.finally()
解决或拒绝状态都会执行
只接受一个参数
目的是避免冗余代码
5.非重入期约代码
非重入特性:当期约进入落定状态时,与之相关的代码仅仅只是被排期,而后面的同步代码必定会先执行
无论如何,都是先处理同步代码
6.邻近处理程序的执行顺序
会按照顺序依次执行
7.传递解决值和拒绝理由
解决值、拒绝理由是作为resolve()、reject()的第一个参数传递的
let p1 = new Promise((reslove,reject) => reslove('foo'))
let p2 = new Promise((reslove,reject) => reject('bar'))
p1.then(value => console.log(value))
p2.catch(reason => console.log(reason))
// 3s后
// foo
// bar
8.拒绝期约与拒绝错误处理
拒绝期约类似throw()表达式,会抛出未捕获错误
但是错误是在消息队列中异步抛出的,同步代码还能继续执行
异步错误只能用onRejected()处理程序捕获
11.2.4 期约连锁和期约合成
1.期约连锁
每个期约方法都能返回一个新的期约对象,因此可以形成期约连锁
let p = new Promise((resolve, reject) => {
console.log('1')
resolve()
})
p.then(() => console.log('2'))
.then(() => console.log('3'))
.then(() => console.log('4'))
// 1
// 2
// 3
// 4
把例子改写就能执行异步任务了,让每一个执行器都返回一个期约实例
let p = new Promise((resolve, reject) => {
console.log('1')
setTimeout(resolve, 1000)
})
p.then(
() =>
new Promise((resolve, reject) => {
console.log('2')
setTimeout(resolve, 1000)
})
)
.then(
() =>
new Promise((resolve, reject) => {
console.log('3')
setTimeout(resolve, 1000)
})
)
.then(
() =>
new Promise((resolve, reject) => {
console.log('4')
setTimeout(resolve, 1000)
})
)
// 1
// 2
// 3
// 4
这样子就解决了之前的回调地狱问题
2.期约图
二叉树的层序遍历
let A = new Promise((resolve, reject) => {
console.log('A')
resolve()
})
let B = A.then(()=> console.log('B'))
let C = A.then(()=> console.log('C'))
let D = B.then(()=> console.log('D'))
let E = B.then(()=> console.log('E'))
let F = C.then(()=> console.log('F'))
3.Promise.all() Promise.race()
Promise.all()会在一组期约全部解决之后再执行
该静态方法接收一个可迭代对象,会把可迭代对象中的元素通过Promise.resolve()转化成期约
let p1 = Promise.all([
Promise.resolve(3),
Promise.resolve(4),
Promise.reject(5)// 一次拒绝会导致合成的期约拒绝
])
Promise.race()和Promise.all()类似
返回第一个解决或拒绝的对象
4.串行期约的合成
基于后续期约使用之前期约的返回值来串联期约时期约的基本功能,很想函数合成
可以提炼出一个通用的合成函数
function addTwo(x) {
return x + 2
}
function addThree(x) {
return x + 3
}
function addFour(x) {
return x + 4
}
function compose(...fns) {
return (x) => fns.reduce((promise, fn) => promise.then(fn), promise.resolve(x))
}
let addTen = compose(addTwo,addThree,addFour)
addTen(8).then(console.log)
11.2.5 期约拓展
ES6的期约有两个未涉及的特性:
1.期约取消
ES6的期约只要开始执行,就无法停止直到完成
这个特性在现有实现基础上提供一种临时封装就可以实现
class CancelToken{
constructor(cancelFn){
this.promise = new Promise((resolve,reject)=>{
cancelFn(resolve)
})
}
}
2.期约进度通知
可以拓展Promise类,为它添加notify()方法
class TrackablePromise extends Promise {
constructor(executor){
const notifyHandlers = []
super((resolve,reject)=>{
return executor(resolve,reject,(status)=>{
notifyHandlers.map((handler) => handler(status))
})
})
}
notify(notifyHandler){
this.notifyHandlers.push(notifyHandler)
return this
}
}
11.3 异步函数
也成为async/await(语法关键字)
11.3.1 异步函数
async用于声明异步函数,可以让函数拥有异步特征。但总体还是同步求值的
但是如果异步函数使用return返回了值,那么这个值会被Promise.resolved()包装成期约对象
async function foo(){
console.log(1);
return 3;
}
foo().then(console.log)
console.log(2);
在异步函数中使用throw()抛出的错误会返回拒绝的期约,可以被Promise.catch()捕捉到
但是使用Promise.reject()返回的不能被捕捉
await
await关键字在async函数中使用,会暂停异步执行函数后面的代码,等期约执行后再执行
单独的Promise.reject()不会被异步函数捕捉,但是加上await就会使拒绝期约返回
async function foo(){
console.log(1);
await Promise.reject(3)
console.log(4);// 不会执行
}
foo().catch(console.log)
console.log(2);
11.3.2 停止和恢复执行
异步函数不包含await,和普通函数没什么区别
await不只是等待一个值这么简单。当函数执行到可用关键字后,就算后面跟着一个立即可用的值,其余部分也会进入异步队列中
async function foo(){
console.log(2);
await null
console.log(4);
}
console.log(1);
foo()
console.log(3);
11.3.3 异步执行策略
1.实现sleep()
async function sleep(delay){
return new Promise((resolve) => setTimeout(resolve,delay))
}
async function foo(){
const t0 = Date.now()
await sleep(1500)// 暂停
console.log(Date.now()-t0);
}
2.平行执行
使用await可能错过平行加速的机会
async function randomDelay(id){
// 延迟0-1000ms
const delay = Math.random() * 1000
return new Promise((resolve) => setTimeout(() => {
console.log(`${id} finished`);
resolve()
}, delay))
}
async function foo(){
const t0 = Date.now()
for(let i=0;i<5;i++){
await randomDelay(i)
}
console.log(`${Date.now() - t0}ms delay`);
}
foo()
顺序等待了5个随机的事件
优化一下代码
async function randomDelay(id){
// 延迟0-1000ms
const delay = Math.random() * 1000
return new Promise((resolve) => setTimeout(() => {
console.log(`${id} finished`);
resolve(id)
}, delay))
}
async function foo(){
const t0 = Date.now()
const promise = Array(5).fill(null).map((_,i) => randomDelay(i))
for(const p of promise){
console.log(`awaited ${await p}`);
}
console.log(`${Date.now() - t0}ms delay`);
}
foo()
虽然不是按顺序执行的,但是await按顺序收到了每个期约的值
3.串行执行期约
async function addTwo(x) {
return x + 2
}
async function addThree(x) {
return x + 3
}
async function addFour(x) {
return x + 4
}
async function addTen(x){
for(const fn of [addTwo,addThree,addFour]){
x = await fn(x)
}
return x;
}
addTen(8).then(console.log)
4.栈追踪与内存管理
期约与异步函数功能类似,但是在内存中差别很大
JS引擎在创建期约时会尽可能保存调用栈信息,这回占用一些内存,但是可以在抛出错误时由运行时的错误逻辑获取
但是使用异步函数不会带来额外的内存消耗,在重视性能的应用中可以优先考虑