期约
期约基础
- 期约是一个带有状态的对象,pedding(等待),resolved(已解决),rejected(拒绝)
- 期约的状态单向流动,状态的转移不可逆,也就是说当状态从pedding变为resolved之后,就算调用reject(),状态也不会流向rejected。
- 期约的状态是私有的,不可以被外部的js所访问和改变。期约将异步的代码封装起来,以隔绝外部的的同步代码
- 期约状态是私有的,只能在内部的执行器中进行操作(改变期约的状态)。执行器的作用有二。
-
- 初始化期约的异步操作
- 通过执行函数的函数参数(resolve和reject)来改变期约的状态,resolve改期约为完成,reject改期约为拒绝
期约的执行器函数,是同步的。
因为执行函数需要初始化异步操作,所以执行函数的内部其实是同步操作。包括调用resolve/reject改变期约的状态的这异步操作。
这与await不同,await会堵塞就异步函数的执行,交出js的运行线程。所以await等待后面的promise完成,本身就是一个异步操作。
javascript
const fn = async()=>{
const p = new Promise((resolve)=>{
console.log(4)
resolve(9)
})
console.log(p)
console.log(3)
}
console.log(1)
fn()
console.log(2)
// 按照顺序执行
- 同步异步的二元性:
-
- 期约是同步的对象,也是异步执行模式的媒介。同步的try/catch无法捕获异步模式的错误(reject())。
- 异步模式下的操作(不止promise,包括一些网络的操作,和定时器等)的回调和执行以及错误的处理方法,都不是由js本身去执行的。它们是由浏览器运行时放入消息队列去调用的。所以我们无法捕获异步的错误。也就是说,要捕获异步的错误必须通过异步的方法。(onrejected)
注意,我们这里的异步错误指的是,promise的函数参数reject(),暴露出来的异步错误。只能通过onrejected,让浏览器去回调,我们自己无法回调。
await promise实例,这种暴露出的错误是已经被await"解包"了,并丢出一个同步的错误了,所以此时,我们需要用try/catch来捕获,否则就像在异步函数中丢住一个同步错误一样,会使得异步函数返回一个拒绝的promise。
期约的实例方法
ps:期约的实例方法是连接内部异步代码和外部同步代码的桥梁。
- Promise.resolve().then(onResolved(),onRejected() ),期约状态改变时的处理方法。会返回一个已解决的期约实例(resolved),但是抛出异常会返回一个拒绝的期约。
-
- onResolved(value),处理期约状态为成功时的回调。参数为期约执行函数(resolve)的值
- onRejected(reason),处理期约状态为失败的回调,并给出理由
- 注意:无论期约状态为成功或者失败的处理程序,then处理函数, 都会返回一个已解决的期约对象(除非then执行函数中主动返回pedding状态的promise) 。这里的rejected状态仍然返回一个已解决的期约可能会有点反直觉,但是onrejected的处理函数,本质上就是为了捕获异步的错误,所以也不应该在捕获之后再抛出一个拒绝的期约(异步错误)
- then中抛出异常会返回一个拒绝的期约。
javascript
const p = Promise.resolve()
const p1 = p.then(()=>{throw(12)})
// 返回p1:rejected
const p2 = p.then(()=>Error(12))
// 返回 p2:resolved(Error(12)),
//返回一个解决的期约,并把错误包装起来
- catch(()=>{}),内部是处理拒绝期约的回调,本质上就是语法糖,和调用then(...,()=>{})的onRejected()回调一样
- finally(),处理期约落定之后的回调。
-
- 无论是拒绝还是解决的期约都会执行。但是finally,无法区分期约的状态,所以它主要还是用来清理数据,收尾工作。
- 因为finally被设计为一个状态无关的方法,所以它大多是时候状态将是父期约状态的传递。
javascript
const p = Promise.resolve(1)
const p1 = p.finally(()=>2)
console.log(p1) // Promise<resolved> 1
期约的非重入特性:
上面说到期约的执行函数用来初始化异步操作,改变期约的状态。所以这个执行函数其实是同步的,比方说。
const p = new Promise((resolve)=>resolve(12)) ,其实实例p已经是一个解决的期约了,但是它的处理程序(then,catch,finally)+ await 不会进入同步线程,而是异步执行的。其实 , 这些处理程序都是被推入浏览器/node的消息队列执行的。他们是由js运行时保证的。
拒绝期约处理+拒绝错误处理
拒绝期约类似于throw表达式,代表了程序应该中断执行。
一般来说,我们在同步代码中抛出错误,会导致程序的中断执行,但是在期约处理函数中抛出同步错误(throw),其实是在消息队列中异步抛出错误,并不会影响同步代码的执行。
我们可以通过try/catch来捕获这些错误,防止期约状态落定。
- 如果给期约添加了多个处理程序,当期约状态变化时,将会按照顺序执行。
- Promise.all(),Promise.race()
-
- 他们都接受一个可迭代的对象,内部放promise实例或其他(会被当成已经解决的期约),返回一个合成promise实例
- Promise.all()的状态是根据所有内部迭代对象决定的,内部全部的期约都解决时,返回的合成期约也显示为解决,并返回一个按顺序包装实例们reoslve值的数组。
- 当Promise.all中有一个实例表示拒绝,合成期约对象的状态也是拒绝。并且拒绝实例的reason也会变为合成期约实例的reason。
- Promise.race,合成期约对象的状态由最先落定的期约实例决定。当有期约对象落定后,后面所有的落定都会被忽略。
- 注意:Promise.all 和 Promise.race 都是会静默处理第一个拒绝状态之后的所有拒绝期约,不会抛出异步的错误。
- 这也表明,当合成期约落定时候,后续的期约不是不执行了,其实后续的期约的执行函数还是会在消息队列中被取出执行,仅仅是他们的状态被合成期约忽略了而已,我们无法查看。
async /await
- async 让函数具有异步特征,但内部按照同步代码执行,
-
- async函数总是期待返回一个期约对象(不一定是已经解决的) ,如果返回值不是期约对象,就包装成已解决的期约,无返回则为resolve(undefined)
- 当然,当async函数内部抛出错误,函数返回一个拒绝的期约,理由为抛出的错误对象
- 因为async内部(期约的执行函数同理),事实上是同步的,所以我们无法捕获异步错误并把错误包装成拒绝promise对象返回,只能探测到同步throw之类抛出的错误,并把错误理由包装为期约的拒绝理由。
javascript
const p = async ()=>{
Promise.reject()
}
console.log(p) // pedding状态
javascript
const p = async ()=>{
throw(3)
}
p().catch(console.log) // 3
// 此时p为rejected的期约
javascript
const p = async ()=>{
await Promise.reject(12)
}
p().then(null,(e)=>console.log(e)) // √
- await会堵塞异步代码的执行,直至期约解决
-
- await 堵塞异步代码执行,交出js运行时,此时js会往下执行异步函数后面的代码
- 无论是等待会抛出错误的同步操作,还是等待拒绝的期约,都会取出error(unwrap)作为理由,返回拒绝期约
- (await+ throw(err) || await + Promise.reject(err) )==>异步函数返回拒绝期约,并带上错误的理由。
- await发生了什么 ?
-
-
在async/await中,真正发挥作用的是await函数,async仅仅是异步函数的标识符而已
-
当异步代码执行遇到await,js会记录位置并停止异步函数的执行,等到await后面的值可用了就会向运行时消息队列中推入函数,恢复异步函数的执行,所以即使await后面的值立即可用,也会异步求值。
-
当await + 异步期约 ,本质上是进行了两次异步操作,第一次是异步期约的执行,第二次是唤醒当前异步函数。可以当作await本身就是一种异步操作。
-
注意
promise最主要的是,promise的初始化+改变状态都是同步代码,但是它的then,catch之类的执行函数都是异步的,他们的回调都会被推入消息队列中等待执行,而promise.reject()主动改变promise的状态,还是在初始化异步代码中抛出同步的错误,都会使promise的状态变为拒绝,抛出异步的错误,所以我们无法用同步的try/catch捕获,必须统统onRejected回调来处理
await,需要注意他会堵塞异步代码的执行,无论await后面的值是不是立即可可用。
ps:如果对本文内容有疑问,欢迎私信评论~