promise async await

256 阅读6分钟

异步编程允许我们在执行一个长时间任务时,程序不需要等待,而是继续执行之后的代码,直到这些任务完成之后再回来通知你,通常是以回调函数(callback)的形式。 这种编程模式避免了程序的阻塞,大大提高了cpu的执行效率,尤其适用于io密集的,例如需要经常进行网络操作 数据库访问的应用

我们知道在javascript中有两种实现异步的方式,第一种是传统的回调函数,比如我们可以使用 setTimeout() 让一个函数在指定的时间之后执行。

这个函数本身会立刻返回,程序紧接着会执行之后的代码。而我们传入的回调函数则会等到预定的时间才会执行。 需要注意的是 JavaScript从设计之初就是一个单线程的编程语言

setTimeout(()=>{
  console.log('异步')
},1000)
console.log('同步')

即便看上去这里的回调函数和主程序在并发执行,但他们都会运行在同一个主线程中。实际上主线程中还运行了我们写的其他代码。包括界面逻辑,网络请求,数据处理等。虽然之后单个线程在执行。但是这种单线程的异步编程方式其实有诸多优点。

由于所有操作都运行在同一个线程中,因此我们无须考虑线程同步或者资源竞争的问题, 并且从源头上避免了线程之间的频繁切换,从而降低了线程自身的开销。回调函数虽然好理解,但是他有一个明显的缺点。 如果我们需要依次执行多个异步操作

setTimeout(()=>{
  console.log('1s...')
  setTimeout(()=>{
    console.log('2s...')
    setTimeout(()=>{
      console.log('3s...')
    },1000)
  },1000) 
},1000)

当第一个程序执行完毕,在回调函数在执行第二个,然后第三个第四...整个程序会一层接一层的嵌套下去。可读性非常差。这种情况我们叫做函数的回调地狱

为了解决这个问题 Promise应运而生。 而JavaScript中使用Promise的api

fetch('/api/***')

是一个很好地例子,他用来发起一个请求来获取服务器数据,我们可以用它动态更新页面的内容,也就是我们平时说的ajax技术.fetch 会返回一个Promise对象,这里的Promise几乎就是他的字面意思,他代表承诺 承诺这个请求会在未来某一个时刻返回数据

fetch('/api/***').then(res=>{})

随后调用then方法,并传递一个回调函数,如果这个请求在未来成功完成,那么回调函数会被调用 请求的结果也会以参数的形式传递进来。当然如果光是这样Promise和回调函数就没有什么区别了。 其实Promise的优点在于他可以用一种链式结构将多个异步操作串联起来(链式调用)

fetch('/api/***').then(res=>res.json())

比如这里的res.json()方法也会返回一个Promise 他代表在将来某一个时刻 将返回的数据转换成json格式 如果我们想要等到他完成之后在执行其他的操作 我们可以在后面追加一个then

fetch('/api/***').then(res=>res.json()).then(json=>console.log({json}))

Promise的链式调用避免了代码的层层嵌套

fetch('/api/***')
.then(/*...*/)
.then(/*...*/)
.then(/*...*/)
.catch()
.finally()

即便是我们有一个很长的链代码也是向下增长不是向右,可读性会提升很多。 在使用异步操作的时候,会遇到很多错误比如网络问题或者返回的数据格式不对等等 比如我们要捕获这些错误,最简单的方法是附加一个catch在链式结构的末尾。如果之前任何一个阶段发生错误, 那么catch将会被触发,而之后的then不会执行,这和同步变成用到的try/catch块很类似 promise还提供了finally方法,他会在promise链结束后调用,无论失败与否,我们可以在这里做一些清理工作,比如加载动画,可以再finaly中关闭

async await 关键字

简单来说他们是基于promise之上的一个语法糖,可以让异步操作更加简洁 首先我们将函数前面加上 async 变成异步函数 异步函数就是指返回值为Promise对象的函数

async function f(){
  //在异步函数中可以调用其他函数 我们就不需要then 而是使用await
  const result = await fetch('***') 
  //await 会等待Promise 完成之后直接返回最终的结果,result 已经是服务器返回的相应数据了
  const json = await result.json()
  //需要注意的是 await 虽然看上去会暂停函数的执行,但在等待过程中,JavaScript同样可以处理其他的任务,比如更新页面,运行其他程序的代码等
  //这是因为await底层是基于Promise和事件循环机制实现的
}
f() //注:这个函数返回值永远是Promise对象

await 使用时的陷阱 比如

async function f(){
  const a = await fetch('/api/***') 
  const b = await fetch('/api/***')  
}

如果我们分别去await这两个异步操作,虽然不存在逻辑错误,但这样写会打破这两个fetch()操作的并行 因为我们会等到第一个任务执行完成之后才开始执行第二个任务 这里更高效的做法是将所有的Promise用Promise.all组合起来然后再await

async function f(){
  const promiseA =  fetch('***') 
  const promiseB =  fetch('***') 
  const [a,b] = await Promise.all([promiseA,promiseB])
  //
}

修改之后程序运行效率也会直接提升一倍

第二个,如果我们需要在循环中执行异步操作 是不能够直接调用forEach 或者 map 这一类方法的

async function f(){
  [1,2,3].forEach(async (i)=>{
    await Asynchronous()
  })
}

尽管我们在回调函数中写了await 但是这里的forEach 会立刻返回 他并不会暂停等到所有的异步操作都执行完毕 如果我们希望等待循环中的异步操作都一一完成之后才继续执行 那我们还是应该使用传统的for循环

async function f(){
  for (let i of [1,2,3]) {
    await Asynchronous()
  }
  console.log('done')
}

更进一步 如果我们想要循环中的所有操作都并发执行 一更炫酷的写法是 for await

async function f(){
  const promises = [
    Asynchronous(),
    Asynchronous(),
    Asynchronous()
  ]
  for await (let i of promises) {
    //...
  }
  console.log('done')
}

这里的for循环依然会等到所有的异步操作都完成了之后才继续向后执行

第三个注意的是 不能在全局或者普通函数中 直接使用await 关键字 await 只能在异步函数中有效 如果我们想在最外层中使用await,那么需要先定义一个异步函数 然后再函数体重使用它 async await 可以让我们写出更清晰,更容易理解的异步代码