异步编程的利器:JavaScript async/await初学者指南

1,253 阅读7分钟

一、前言

首先我们来看一个问题。

let data = null
function getData() {
  setTimeout(() => {
    data = { name: "张三", age: 20 }
  }, 1000);
}
function showData() {
  console.log(`我正在展示获取的数据: ${JSON.stringify(data)}`)
}
getData()
showData()//我正在展示获取的数据: null

在以上的代码示例中,我们的本意是想让 showData 函数输出它获取到的数据,可是数据却没有获取到,输出的是null,这是因为 getData 函数是异步代码,它在同步代码执行完之后才执行。那有什么办法解决这个问题呢?

二、async/await的引入

一种显而易见的方法就是回调函数,把 showData 放到 getData 函数里执行,也就是如下所示。

let data = null
function getData() {
  setTimeout(() => {
    data = { name: "张三", age: 20 };
    showData()
  }, 1000);
}
function showData() {
  console.log(`我正在展示获取的数据: ${JSON.stringify(data)}`);
}
getData(); //我正在展示获取的数据: {"name":"张三","age":20}

第二种方式就是使用Promise对象,注意then(showData)里面showData不要加括号。

let data = null
function getData(){
  return new Promise((resolve, reject) =>{
    setTimeout(() => {
      data = {name: '张三',age: 20}
      resolve()
    },1000)
  })
 }
function showData(){
  console.log(`我正在展示获取的数据: ${JSON.stringify(data)}`)
 }
getData().then(showData) //我正在展示获取的数据: {"name":"张三","age":20}

那么还有什么方法吗?有的,那就是利用async/await

let data = null
function getData(){
  return new Promise((resolve, reject) =>{
    setTimeout(() => {
      data = {name: '张三',age: 20}
      resolve()
    },1000)
  })
 }
 function showData(){
  console.log(`我正在展示获取的数据: ${JSON.stringify(data)}`)
 }
async function handle(){
  await getData();
  showData();
}
handle(); //我正在展示获取的数据: {"name":"张三","age":20}

并且如果有多个异步函数,就只需要继续await+函数名去调用即可,和原生JS调用函数方法类似,注意await后面所接的代码必须返回Promise对象。接下来我们就聊一聊async/await究竟是什么以及它如何使用?

function getData1(){
  return new Promise((resolve, reject) =>{
    setTimeout(() => {
      console.log({name: '张三',age: 20})
      resolve()
    },1000)
  })
 }
function getData2(){
  return new Promise((resolve, reject) =>{
    setTimeout(() => {
      console.log({name: '李四',age: 21})
      resolve()
    },2000)
  })
 }
function showData(){
  console.log({name: '王五',age: 22})
}
async function handle(){
  await getData1();
  await getData2();
  showData();
}
handle();
//{ name: '张三', age: 20 }
//{ name: '李四', age: 21 }
//{ name: '王五', age: 22 }

三、async/await的基本语法

async/await是JavaScript中处理异步操作的一种方式。它们是ES2017中引入的新关键字,可以让开发人员更方便地处理异步代码。async/await本质上是Promise的语法糖,它们使得异步代码更加易于理解和维护。

async/await的使用需要遵循一定的规则,主要有以下几点:

  • 1.在需要异步执行的函数前加上async关键字,这样该函数就会返回一个Promise对象。
  • 2.在需要等待异步操作的地方使用await关键字,该关键字会暂停下面代码的执行,直到Promise对象返回结果。
  • 3.在使用await关键字时,必须在一个async函数中,否则会抛出语法错误,但是async函数中可以没有await。

1. async返回一个Promise对象

async function asyncFn() {
  return 'Hello World!';
}
const result = asyncFn()
console.log(result);
asyncFn().then((value) => {
  console.log(value); //Hello World!
})

QQ截图20230507211828.jpg

在上面的代码示例中,在函数前面加上async关键字后,返回的结果不是字符串,而是一个Promise对象,并且状态为fulfilledasync函数内部return语句返回的值,会被then方法回调函数接收到,会成为then方法回调函数的参数。

2. 抛出的错误对象会被catch收到

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

async function fn() {
  throw new Error('发生错误啦!');
}
fn().then(
  (value) => console.log('resolve', value),
  (error) => console.log('reject', error) //reject Error: 发生错误啦!
)  //(代码会报错)
async function fn() {
  await Promise.reject('发生错误啦!');
}
fn()
  .then(value => console.log(value))
  .catch(error => console.log(error)) //发生错误啦!

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。下面代码中,第二个await语句是不会执行的,因为第一个await语句状态变成了reject。

async function asyncFn() {
  await Promise.reject('出错了');
  await Promise.resolve('Hello World!'); //不会执行
}

3. try...catch语句捕获处理抛出的异常

await的promise失败了,就会抛出异常,需要通过try...catch语句捕获处理。 如果我们希望即使前一个异步操作失败,也不要中断后面的异步操作,就可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

async function asyncFn() {
  try {
    await Promise.reject('发生错误啦!');
  } catch(e) {
    console.log(e); 
  }
  return await Promise.resolve('Hello World');
}
asyncFn()
    .then(value => console.log(value)) 
//发生错误啦!
//Hello World

也可以这样写。

async function asyncFn() {
  await Promise.reject('发生错误啦!')
    .catch(e => console.log(e));
  return await Promise.resolve('Hello World!');
}
asyncFn()
    .then(value => console.log(value)) 
//发生错误啦!
//Hello World!

四、async/await考题

我们来看三道题目来加深一下对async/await的理解。

1.题目一

function fn(){
  return new Promise((resolve, reject) => {
    setTimeout(function(){
      console.log(666)
      resolve()
    },1000)
  })
}
async function asyncFn(){
  await fn()
  console.log(777)
}
asyncFn()
//666
//777

在以上代码示例中,首先我们明确输出顺序是 666 777,因为await会阻止后面代码的执行,等执行完fn函数后才会去执行后续代码。这比较简单,那么如何将async/await转化成Promise形式呢?

function asyncFn(){
  return Promise.resolve()
  .then(() => {
    return fn()
  })
  .then(() => {
    console.log(777)
  })
} //666 777

async/await和Promise是两种处理异步编程的方式,它们之间并不是互斥的,而是可以互相转换的。如果你有一个使用async/await编写的异步函数,并且需要将其转换为Promise形式,那么可以使用Promise.resolve方法来实现,所以以上asyncFn函数可以写成两种形式,输出结果一样。

2.题目二

async function fn1(){
  return 666
}
async function fn2(){
  return new Promise((resolve, reject) => {
    resolve(666)
  })
}

简单来说,在函数前面加一个async就相当于return了一个new Promise。以上两个函数转化成Promise形式相当于如下。

function fn1(){
  return Promise.resolve().then(() => {
    return 666
  })
}
function fn2(){
  return Promise.resolve().then(() => {
    return new Promise((resolve, reject) => {
      resolve(666)
    })
  })
}
async function asyncFn() {
  let a = 1
  let b = 2
  await fn1()
  let c = 3
  await fn2()
  let d = 4
  await fn3()
  return 5
}

第一、将写在await后面的代码放到async创建的那个Promise里面执行。 第二、将写在await下面的代码放到前一个创建的那个Promise对象的.then里面执行。await返回的也是Promise对象,它把await下面的代码放到了await返回的promise的.then里面执行。

function asyncFn() {
  return Promise.resolve().then(() => {
      let a = 1
      let b = 2
      return fn1()
  })
  .then(() => {
      let c = 3
      return fn2()
  })
  .then(() => {
      let d = 4
      return fn3()
  })
  .then(() => {
      return 5
  })
}

3.题目三

async function async1(){
  console.log('111 开始');
  await async2()
  console.log('111 结束');
}
async function async2(){
  console.log('222');
}
async1()
console.log('666');
//111 开始
//222
//666
//111 结束

比较新的版本的浏览器会对await开辟一个优先通道,把它的优先级提高了。哪怕async2执行可能需要耗时,也会优先执行它,await下面的那行代码依然会被阻塞,所以输出结果如上。

五、async/await的应用

1.读取文件内容

const fs = require('fs');
function readTengWangGe() {
  return new Promise((resolve, reject) => {
    fs.readFile("./滕王阁序.md", (error, data) => {
      //如果失败
      if(error) reject(error);
      //如果成功
      resolve(data);
    })
  })
}
function readLuShan() {
  return new Promise((resolve, reject) => {
    fs.readFile("./望庐山瀑布.md", (error, data) => {
      //如果失败
      if(error) reject(error);
      //如果成功
      resolve(data);
    })
  })
}
async function main(){
  let tengWangGe = await readTengWangGe();
  let luShan = await readLuShan();
  console.log(tengWangGe.toString());
  console.log(luShan.toString());
}
main();
//落霞与孤鹜齐飞,秋水共长天一色。
//飞流直下三千尺,疑是银河落九天。

这里是简单模拟async/await异步读取文件内容,文件就放在同级目录中,内容分别是落霞与孤鹜齐飞,秋水共长天一色,和飞流直下三千尺,疑是银河落九天

2.发送Ajax请求

function sendAjax(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.send();
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if(xhr.status === 200 && xhr.status < 300) {
          resolve(xhr.response) // 如果成功
        }else{
          reject(xhr.status)  // 如果失败
        }
      }
    };
  });
}
async function test(){
  const result = await sendAjax("https://www.fastmock.site/mock/7224291cf063beceaa27956d01e64560/keep/recommend")
  console.log(JSON.stringify(result));
}
test()

QQ截图20230508104143.jpg 这里是用async/await模拟发送Ajax请求数据。注意以上代码是写在html文件中的,然后用浏览器查看。步骤是:1.创建XMLHttpRequest对象。2.初始化。3.发送。4.事件绑定。发送Ajax请求,返回的结果是Promise对象。

readyState表示就绪状态,xhr.readyState === 4表示请求已经完成并且响应已经准备就绪。status表示请求的HTTP状态码,如果请求成功(状态码为 200),并且状态码小于 300,也就是请求返回的是一个成功的响应(2xx 表示成功),则执行相应的代码块。

六、总结

总之,async/await是一种强大的异步编程技术,它可以使异步代码更加容易理解和维护。通过理解async/await的语法和注意事项,开发人员可以更好地应用这一技术,提高代码的可读性和可维护性。 如果对Promise不了解的,可以去看# 从回调地狱到异步之王:小白初次感受JS Promise的魅力!

能力一般,水平有限,如有问题欢迎指正,感谢你阅读这篇文章,如果你觉得写得还行的话,不要忘记点赞、评论、收藏哦!祝生活愉快!