浅谈async/await

背景

ES7提出的async/await是JavaScript为了解决异步问题而提出的一种解决方案,没有更多的回调,许多人将其称为异步的终极解决方案。async函数是Generator函数的语法糖。使用关键字async表示,在函数内部使用await表示异步。JavaScript的发展也经历了回调、Promise、async/await三个阶段,该文章写了我自己对于async/await的理解。不对的地方还请大家帮忙指出,共同进步。

概念

async是“异步”的简写,async申明一个function是异步的,而await是等待一个异步方法执行完成,await 只能出现在async函数中。
async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

  • async的作用
    async函数负责返回一个Promise 对象;如果在async函数中return一个变量,async 会把这个变量通过Promise.resolve() 封装成Promise对象;如果 async函数没有返回值,它会返回Promise.resolve(undefined)。
  • await在等待什么

一般我们都用await等待一个async函数完成,await 等待的是一个表达式,这个表达式的计算结果是Promise 对象或者其它值,所以,await后面实际可以接收普通函数调用或者直接量。

如果await等到的不是一个promise对象,那跟着表达式的运算结果就是它等到的东西;如果是一个promise对象,await会阻塞后面的代码,等promise对象resolve,得到resolve的值作为await表达式的运算结果
虽然await阻塞了,但await在async中,async不会阻塞,它内部所有的阻塞都被封装在一个promise对象中异步执行。

优点

不同于Generator的是,async函数的改进在下面四点:

  • 内置执行器。Generator 函数的执行必须依靠执行器,而async函数自带执行器,调用方式跟普通函数的调用一样。
  • 更好的语义。async和await 相较于*和yield更加语义化。
  • 更广的适用性。co模块约定,yield命令后面只能是Thunk函数或 Promise对象。而async 函数的await命令后面则可以是Promise或原始类型的值(Number,string,boolean,但这时等同于同步操作)。
  • 返回值是Promise, async函数返回值是Promise对象,比Generator函数返回的Iterator对象方便,可以直接使用then()方法进行调用。

特点

  • 建立在promise之上,所以不能把它和回调函数搭配使用,但它会声明一个异步函数,并隐式返回一个Promise。因此可以直接return变量,无需使用Promise.resolve进行转换。
  • 和promise一样,是非阻塞的,但不用写then及其回调函数,减少代码行数,避免了代码嵌套,而且,所有异步调用可以写在同一个代码块中,无需定义多余中间变量。
  • 最大价值在于可使异步代码在形式上更接近同步代码。
  • 总是与await一起使用的,并且,await只能在async函数体内
  • await是个运算符,用于组成表达式,会阻塞后面代码。若等到的是Promise对象,则得到其resolve值, 否则,得到一个表达式的运算结果。

async/await使用规则

我们在处理异步时,比起回调函数,Promise的then方法会显得比较简洁和清晰,但是在处理多个彼此之间相互依赖的请求时,会显的有些累赘。这个时候,用async和await会更加优雅。

  • 规则一:凡是在前面添加async的函数在执行后都会自动返回一个Promise对象。
async function fun() {}
let result = fun()
console.log(result)  //即使代码fun函数什么都没返回,依然会打印出Promise对象
  • 规则二:await必须在async函数里使用,不能单独使用
async fun() {
   let result = await Promise.resolve('success')
   console.log(result)
}
fun()
  • 规则三:await后面需要跟Promise对象,不然就没有意义,而且await后面的Promise对象不必写then,因为await的作用之一就是获取后面Promise对象成功状态传递出来的参数。
function fun() {
   return new Promise((resolve, reject) => {
       setTimeout(() => {resolve('hello javascript')})
   })
}
async fn() {
   let result = await fun() //fun会返回一个Promise对象
   console.log(result)    //打印出Promise成功后传递过来的'hello javascript'
}

fn()

也可以这么写,但是意义不大:

let fun = async function() {
  let result = await 123
  console.log(result)
}
fun()

语法

async函数返回一个Promise 对象
async函数内部return返回的值,是then方法回调函数的参数。

async function  fun() {
    return 'hello node'
};
fun().then( (v) => console.log(v)) // hello node

如果async函数内部抛出异常,则会导致返回的Promise对象状态变为 reject状态。抛出的错误会被catch方法回调函数接收到。

async function fun(){
    throw new Error('error');
}
fun().then(v => console.log(v))
.catch( e => console.log(e));

async函数返回的Promise对象,必须等到内部所有的await命令的Promise对象执行完,才会发生状态改变
即只有当async函数内部的异步操作都执行完,才会执行then方法的回调

如下代码所示:

const time = timeout => new Promise(resolve=> setTimeout(resolve, timeout));
async function f(){
    await time(1000);
    await time(4000);
    await time(7000);
    return 'finish';
}

f().then(v => console.log(v)); // 等待12s后才输出 'finish'

正常情况下,await 命令后面跟着的是 Promise ,如果不是的话,也会被转换成一个立即resolve的Promise
如下代码所示:

async function  fun() {
    return await 1
};
fun().then( (v) => console.log(v)) // 1

如果返回的是reject的状态,会被catch方法捕获。

async函数的错误处理

其实async函数语法不难,难就在错误处理上。有关错误处理,如上面规则三所说,await可直接获取到后面Promise成功状态传递的参数,但却捕捉不到失败状态。我们通过给包裹await的async函数添加then/catch方法来解决,因为根据规则一,async函数本身就会返回一个Promise对象。

我们先来看一个demo

let b;
async function fun() {
    await Promise.reject('error');
    b = await 1; // 这段await并未执行
}
fun().then(v => console.log(b));
注意:当async函数中只要一个await出现reject状态,则后面的await都不会被执行。解决办法:可以添加try/catch。

正确的写法如下:

// 正确的写法
let b;
async function fun() {
    try {
        await Promise.reject('error')
    } catch (error) {
        console.log(error);
    }
    b = await 1;
    return b;
}
fun().then(v => console.log(b)); // 1

如果有多个await则可将其都放在try/catch中。
我们来看一个包含错误处理的完整demo:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => {
      let random = Math.random()
      if (random >= 0.5) {
          resolve('成功')
      } else {
          reject('失败')
      }   
  }, 1000)
})
async function fn() {
  let result = await promise
  //result是promise成功状态的值,如果失败了,代码就直接跳到下面的catch了
  return result 
}
fn().then(response => {
  console.log(response) 
}).catch(error => {
  console.log(error)
})
// 最终打印出成功
注意:上述代码需注意两个地方,一是async函数需要主动return,如果Promise的状态是成功的,那么return的这个值就会被下面的then方法捕捉到;二是如果async函数有任何错误,都被catch捕捉到!

同步与异步

在async函数中使用await,await这里的代码就会变成同步,意思就是只有等await后面的Promise执行完成得到结果才会继续下去,await就是等待,虽然这样避免了异步,但是它会阻塞代码,所以我们在使用的时候需要考虑周全。
我们来看个demo:

function fun1(name) {
  return new Promise((resolve, reject) => {
      setTimeout(() => {
          resolve(`${name}成功执行`)
      }, 1000)
  })
}
async function testfun2() {
  let p1 = await fun1('张三')
  let p2 = await fun1('李四')
  let p3 = await fun1('王麻子')
  return [p1, p2, p3]
}
testfun2().then(result => {
  console.log(result)
}).catch(result => {
  console.log(result)
})

这样写虽然是ok的,但是await会阻塞代码,每个await都必须等后面的fun1()执行完成才会执行下一行代码,所以testfun2函数执行需要3秒。如果不是遇到特定的场景,最好不要这样用。

循环中使用async/await

在循环中使用await,需要牢记一条:必须在async函数中使用。
在for…of中使用await,我们来看个demo:

let fun3 = (time) => {
  return new Promise((resolve) => {
      setTimeout(() => {
          resolve(time)
      }, time)
  })
}
let times = [1000, 2500, 3000]
async function testfun3() {
  let result = []
  for (let item of times) {
      let temp = await fun3(item)
      result.push(temp)
  }
  return result
}
testfun3().then(result => {
  console.log(result)
}).catch(error => {
  console.log(error)
})
// 最后输出[ 1000, 2500, 3000 ]

async异步回调并发

1请求、2请求同时发,规定请求到达的顺序
假如我们有一种这样的业务需求,并发两个请求,但是要规定收到请求的顺序应该怎么做的?这里借鉴阮一峰大神的代码:
我们看下demo:

  // 1请求
  function getData1 () {
    return new Promise(function (resolve, reject) {
      setTimeout(() => {
        console.log('1执行了')
        resolve('请求到模拟数据1')
      }, 2000)
    })
  }
  // 2请求
  function getData2 () {
    return new Promise(function (resolve, reject) {
      setTimeout(() => {
        console.log('2执行了')
        resolve('请求到模拟数据2!)
      }, 1500)
    })
  }
  async function asyncDemo2 () {
    const arr = [getData1, getData2]
    const textPromises = arr.map(async function (doc) {
      const response = await doc()
      return response
    })
    // 按次序输出
    for (const textPromise of textPromises) {
      console.log(await textPromise);
    }
  }
  // 2执行了 (因为2是1500ms后执行) 所以2先执行
  // 1执行了
  // 请求到模拟数据1  (for .. of )规定了输出的顺序
  // 请求到模拟数据2

适用async/await的业务场景

在前端开发中,我们偶尔会遇到这样的场景:我们需要发送多个请求,而且后面请求的发送总是需要依赖上一个请求返回的数据。对于这个问题,我们既可以用Promise的链式调用来解决,也可用async/await来解决,然而后者会更简洁些。
我们来看个demo:
使用promise链式调用处理下面代码:

function fun2(time) {
  return new Promise((resolve, reject) => {
      setTimeout(() => {
          resolve(time)
      }, time)
  })
}
fun2(500).then(result => {
  return fun2(result + 1000)
}).then(result => {
  return fun2(result + 1000)
}).then(result => {
  console.log(result)
}).catch(error => {
  console.log(error)
}) 
// 最终结果是2500

使用async/await来处理:

function fun2(time) {
  return new Promise((resolve, reject) => {
      setTimeout(() => {
          resolve(time)
      }, time)
  })
}
async function getResult() {
  let p1 = await fun2(500)
  let p2 = await fun2(p1 + 1000)
  let p3 = await fun2(p2 + 1000)
  return p3
}
getResult().then(result => {
  console.log(result)
}).catch(error => {
  console.log(error)
})

从上述代码中我们可以看出,相对于使用then不停地进行链式调用, 使用async/await显的更加简洁清晰明了易读一些。

async相对于Promise的优势

  • 能更好地处理then链
  • 中间值
  • 调试,相比于 Promise 更易于调试

我们总结一下async/await的优点:

  • 解决了回调地狱的问题
  • 支持并发执行
  • 可以添加返回值 return 变量
  • 可以在代码中添加try/catch捕获错误
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章