您现在的位置是:首页 >技术杂谈 >彻底理解Promise和async/await网站首页技术杂谈
彻底理解Promise和async/await
Promise
1.异步行为是为了优化因计算量大而时间长的操作.
2.pedding 待定: 表示尚未开始或正在进行中
fulfilled 解决: 表示已经成功完成
rejected 拒绝: 表示没有完成
3.从pedding状态切换到fulfilled状态或rejected状态后,状态就不会再改变.而且也不能保证promise比如会脱离待定状态.
因此,无论promise是resolve还是reject,甚至永远处于待定状态,都应该具有恰当的行为
4.执行器函数的两项职责: 初始化promise的异步行为和控制状态的最终转换.
控制promise状态的转换是通过resolve()和reject()来实现的
resolve()会把状态切换为兑现
reject()会把状态切换为拒绝,reject()会抛出错误.
5. 执行器函数是同步执行的, 因为执行器函数是promise的初始化程序
console.log(1);
new Promise((resolve,reject) => {
console.log(3);
})
console.log(2);
//打印1, 3, 2
6.无论resolve()和reject()中的哪个被调用,状态转换后都不可撤销. 继续修改状态会默认失败
const p = new Promise((resolve,reject) => {
resolve('666')
reject('000') // 没有效果
})
console.log(p); // 666
7.为了避免promise卡在待定状态,可以添加一个定时退出功能,通过setTimeout设置一个10秒后无论如何都会拒绝promise的回调
如果执行器中的代码在超时之前已经解决或拒绝,那么再次调用reject也会默认失败
8.Promise.resolve()
通过Promise.resolve()静态方法,可以实例化一个解决的Promise
使用这个静态方法实际上可以把任何值都转换为一个Promise
const p1 = new Promise((resolve,reject) => resolve())
const p2 = Promise.resolve() // undefined
const p3 = Promise.resolve(3) // 3
const p4 = Promise.resolve(4,5) // 4 多余参数会忽略, 可以使用对象
9.Promise.reject()
实例化一个拒绝的promise并抛出一个异步错误(这个错误不能通过try/catch捕获,只能通过拒绝处理程序捕获)
const p = Promise.reject(4) // promise <rejected> 4
10. Promise的实例方法
10.1.实现Thenable接口
10.2.Promise.prototype.then()
Promise.prototype.then()是为Promise实例添加处理程序的主要方法.
then()最多接收两个参数 onResolved处理程序和onRejected处理程序,这两个参数都是可选的分别进入兑现和拒绝状态
let p1 = new Promise((resolve,reject) => setTimeout(resolve,2000))
let p2 = new Promise((resolve,reject) => setTimeout(reject,2000))
p1.then(() => onResolved('p1'),
() => onRejected('p1'))
p2.then(() => onResolved('p2'),
() => onRejected('p2'))
function onRejected(id){
console.log(id,'rejected');
}
function onResolved(id){
console.log(id,'onResolved');
}
因为promise的只能转换最终状态一次,所以这两个操作一定的互斥的.
传给then()的任何非函数类型的参数都会被静默忽略.如果只提供onRejected参数,要在onResolved位置上传入undefined
p1.then('glsdfosdfisdjif') // 会被忽略,不推荐
p1.then(null,() => onRejected('p2')) // 不传onResolved的规范写法
Promise.prototype.then()返回一个新的Promise实例, 会通过Promise.resolve()隐式包装来生成新的Promise
const p3 = Promise.resolve('wei') // 默认返回undefined
10.3.Promise.prototype.catch()
Promise.prototype.catch()是为Promise实例添加拒绝处理程序.
实际上这个方法就是一个语法糖,相当于调用了 Promise.prototype.then(null,onRejected)
返回一个新的Promise实例,会通过Promise.resolve()隐式包装来生成新的Promise
10.4.Promise.prototype.finally()
Promise.prototype.finally()用于给promise添加onFinally处理程序,在promise转换为解决或者拒绝都会执行, 主要是避免在resolve和rejected中出现冗余代码. 但是没办法知道promise的状态是解决还是拒绝.
返回一个新的Promise实例,不同于then()和catch()方法返回的实例.在大多数情况下它将表现为父Promise的传递.
10.5.非重入Promise方法
当Promise进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行.(就是被放到一个微任务里去了)
// 例子
const p = Promise.resolve()
p.then(() => {
console.log('p.then()');
})
console.log('同步执行');
// 实际输出 同步执行, p.then()
// 例子
let sy;
let p = new Promise((resolve) => {
sy = function() {
console.log(1);
resolve()
console.log(2);
}
})
p.then(() => {
console.log(4);
})
sy()
console.log(3);
/*
实际输出: 1,2,3,4 即使Promise的状态发生在添加处理程序之后,
处理程序也会等到运行的消息队列让出时,才会执行.
*/
10.6.临近处理程序的执行顺序
10.7.传递解决值和拒绝理由
解决的值和拒绝的理由分别通过resolve()和reject()的第一个参数往后传,直到报错
Promise.resolve() 和Promise.reject() 在被调用时就会接受解决值和拒绝理由.
10.8.拒绝Promise与拒绝错误处理
拒绝Promise类似于throw()表达式.它们都代表一种程序状态, 即需要中断或者特殊处理
let p1 = new Promise((resolve,reject) => reject( Error('1')))
let p2 = new Promise((resolve,reject) => { throw Error('2') })
let p3 = Promise.resolve().then(() => { throw Error('3') })
let p4 = Promise.reject(() => Error('4'))
console.log(p1);
console.log(p2);
console.log(p3);
console.log(p4);
Promise.resolve().then()的错误最后才出现.
正常情况下,在通过throw()关键字抛出错误时,js运行时的错误处理机制会停止执行抛出错误之后的任何代码(抛出错误之后不解析了)
// 正常情况xxx不会执行
throw Error()
console.log('xxx');
// promise抛出错误时,因为错误实际上是从消息队列中抛出来的,所有不会阻止运行时
Promise.reject(Error('x'))
console.log('xxx'); // 会输出
// 例子:
let p = new Promise((resolve,reject) => {
console.log(1);
reject(Error())
}).then(() => {
console.log(2);
}).catch((e) => {
console.log(3);
}).catch(() => {
console.log(4);
}).then(() => {
console.log(5);
})
/*
实际输出1,3,5 首先打印1, 没毛病, 接下来执行reject(),打印3, catch 函数会隐式的调用
Promise.resolve()函数, 因此继续.then()打印5
*/
10.9.Promise连锁与合成
多个Promise组合在一起可以构成强大的代码逻辑.
通过两种方式实现: promise连锁和promise合成,前者是一个promise接一个promise的拼接,后者则是将多个promise组合成一个promise
//要执行真正的异步任务,让每个执行器都返回一个promise实例
let p1 = new Promise((resolve,reject) => {
console.log(1);
setTimeout(resolve,1000)
})
p1.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 1秒后
// 2 2秒后
// 3 3秒后
// 4 4秒后
Promise的处理程序是按照它们添加的顺序执行的,由于Promise的处理程序是先添加到消息队列, 然后才逐个执行.
将多个Promise实例组合成一个Promise的静态方法. Promise.all()和Promise.race()
Promise.all()方法创建的promise会将一组promise全部解决后,再返回结果.
Promise.all()接收一个可迭代对象,返回一个新Promise
let p1 = Promise.all([
Promise.resolve(),
Promise.resolve()
])
// 可迭代对象中的元素会通过Promise.resolve()转换为Promise
let p2 = Promise.all([3,4])
// 空的可迭代对象等价于Promise.resolve()
let p2 = Promise.all([])
// 无效的语法
let p2 = Promise.all() // 报错
有一个Promise待定,则合成的Promise也会待定, 有一个Promise拒绝,则合成的Promise也会拒绝.
// 一次拒绝导致最终Promise的拒绝
let p1 = Promise.all([
Promise.resolve(),
Promise.resolve(),
Promise.reject()
])
// 如果所有的promise都成功解决,则合成promise的解决值就是所有包含promise解决值的数组(按迭代器顺序)
let p1 = Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve()
])
p1.then(res => {
console.log(res); [1, 2, undefined]
})
// 多个reject的情况
let p1 = Promise.all([
Promise.reject(1),
Promise.reject(2),
Promise.resolve()
])
p1.then(res => {
console.log(res);
/*
1 会将第一个拒绝的理由作为合成promise的拒绝理由. 之后再拒绝的promise的不会
影响最终promise的拒绝理由.
*/
})
Promise.race()方法创建的任意一个 Promise 对象状态变为 fulfilled 或 rejected 时立即返回该 Promise 对象的值或原因。
语法同all()
只要第一个落定的promise,Promise.race()就会包装其解决值或姐拒绝理由并返回新promise
ES6不支持取消Promise和进度通知,一个主要原因就是这样会导致promise连锁和promise合成过渡复杂化.
async await
功能: 让以同步方式写的代码能够异步执行.
async/await解决利用异步结构组织代码的问题.
1. async 关键字用于声明异步函数 可以用在函数声明,函数表达式,箭头函数和方法上
async function foo() {}
let bar = async function() {}
let baz = async () => {}
class Qux {
async qux(){}
}
使用async关键字可以让函数具有异步特征,但总体上代码仍然是同步求值的.
而在参数或闭包方面,异步函数仍然具有普通函数js函数的正常行为.
例子:
async function foo() {
console.log('1');
}
foo()
console.log(2);
// 1,2
异步函数如果使用return 关键字返回了值(没有return 返回undefined),这个值会被Promise.resolve()包装成一个Promise对象.
async function foo() {
console.log('1');
// return 666
return Promise.resolve(666) // 效果同上
}
// 给返回的promise添加一个解决处理程序
foo().then(console.log)
// 1, 666
在异步函数中抛出错误会返回拒绝的Promise
async function foo() {
console.log('1');
throw 3
console.log('4'); // 不执行
}
// 给返回的promise添加一个解决处理程序
foo().catch(console.log)
console.log(2);
// 1, 2, 3
2. 异步函数主要针对不会马上完成的任务,需要一种暂停和恢复执行的能力.
使用await关键字可以暂停异步函数代码的执行,等待Promise的解决.它可以单独使用,也可以在表达式中使用
await命令后面是一个Promise对象. 如果不是,会被转成一个立即resolve的Promise对象.
// 对拒绝promise使用await会释放错误值(将拒绝promise返回)
async function foo() {
console.log('1');
await Promise.reject(3)
// return await Promise.resolve(3)
// resolve需要return then才能接受, 而reject不需要return catch也可以接收
console.log('4'); // 这行代码不会执行
}
// 给返回的promise添加一个解决处理程序
foo().then(console.log).catch(console.log)
console.log(2);
// await Promise.reject(3) 打印输出1,2,3
只要一个await语句后面的Promise变成reject, 那么整个async函数都会中断执行.
// 下面await语句是不会执行的,因为第一个await已经变成了reject
async function foo() {
await Promise.reject('出错了')
await Promise.resolve('hello') // 不会打印
}
如果希望前一个异步操作失败,也不要中断后面的异步操作.
这时可以将第一个await放在try...catch 这也不管这个异步是否成功,第二个await都会执行
async function foo() {
try {
await Promise.reject('出错了') // 会打印
} catch (e) {
console.log(e);
}
return await Promise.resolve('hello') // 会打印
}
foo().then(v => console.log(v))
另一种方式是在await后面的promise对象后添加一个catch方法,处理前面可能出现的错误
async function foo() {
await Promise.reject('出错了').catch (e => console.log(e))//会打印
return await Promise.resolve('hello') // 会打印
}
foo().then(v => console.log(v))
3.await 的限制
await关键字必须在异步函数中使用.
不允许:await出现在箭头函数中
不允许:await出现在同步函数声明中
不允许:await出现在同步函数表达式中
不允许:IIFE使用同步函数表达式或箭头函数
停止和恢复执行
使用await关键字之后的区别其实看上去还要微妙一些.
async function foo() {
console.log(await Promise.resolve('foo'));
}
async function bar() {
console.log(await 'bar');
}
async function baz() {
console.log('baz');
}
foo()
bar()
baz()
// 打印 baz, bar, foo
async/await中真正起作用的是await. async只是一个标识符,毕竟异步函数如果不包含await关键字,基本和普通函数没什么区别
async function foo() {
console.log(2);
}
console.log(1);
foo()
console.log(3);
// 打印 1, 2, 3
要完全理解await关键字,必须知道它并非只是等待一个值可用那么简单.
js运行时在碰到await关键字时, 会记录在哪里暂停执行. 等到await后面的值可用了,js运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行
因此, 即使await后面跟着一个立即可用的值,函数的其余部分也会被异步求值.下面例子:
async function foo() {
console.log(2);
await null
console.log(4);
}
console.log(1);
foo()
console.log(3);
// 打印 1, 2, 3, 4
/*
运行时的工作过程:
(1) 打印1;
(2) 调用异步函数foo();
(3) (在foo中)打印2;
(4) (在foo中)await关键字暂停执行,为立即可用的值null向消息队列中添加一个任务;
(5) foo()退出;
(6) 打印3;
(7) 同步线程执行完毕;
(8) js运行时从消息队列中取出任务,恢复异步函数执行;
(9) (在foo中)恢复执行,await取得null值(这里并没有用)
(10) (在foo中)打印4;
(11) foo()返回
*/
// TC39对await后面是promise的情况如何处理做过一次修改. 修改后await Promise.resolve(8)只会生成一个异步任务
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);
// 打印 1, 2, 3, 4, 5, 8, 9, 6, 7
使用注意点:
1.await命令后面的promise对象的运行结果可能是rejected,最好把await放在try...catch代码块中
2.多个await命令后面的异步操作如果不存在继发关系,最好让他们同时触发
// getFoo和getBar是两个独立的异步操作,被写成激发关系,这样比较耗时.可以让它们同时触发
let foo = await getFoo()
let bar = await getBar()
let [foo, bar] = await Promise.all([getFoo,getBar])
async函数的实现原理:
async函数的实现原理就是将Generator函数和自动执行器包装在一个函数里.
面试官: 来说一下你对Promise的理解?
个人对Promise的理解是, promise是一种异步编程的解决方案, 它比传统的回调函数加事件更加合理合强大,
目前除了使用promise的异步操作外,还使用promise在项目中解决回调地狱等问题.
promise是一个对象可以获取异步操作的信息
promise的特点:
对象不受外界影响,promise一共有三个状态, 分别是进行中,成功和失败,只有异步操作的结果,可以决定是哪一种状态,任何其他的操作都无法改变这个状态.
一旦状态改变就不会在变,任何时候都可以得到这个结果,promise的状态改变只有两种可能要么成功要么失败
如果要使用promise必须对promise进行实例化, 实例化之后promise内有一个回调函数,这个函数有两个参数,分别是resolve和reject,
当我们的状态发生变化的时候,如果是成功则会通过resolve将成功的结果返回,如果失败通过reject将错误的信息返回出去.
通过.then方法接收成功的结果,通过.catch方法接收失败的结果
promise常用的方法还用promise.all(),race()方法. 主要是将多个实例包装成一个新的实例.
Promise.all()方法创建的promise会将一组promise全部解决后,再返回结果.
Promise.race()方法哪个接口跑的快,先返回哪个
在项目中一般使用promise来对接口进行封装,以及一些异步的操作都会用到promise.校验