掌握异步编程高级技巧: 从Promise到Async/Await

来源

https://www.bilibili.com/video/BV1zq4y1p7ga/?spm_id_from=333.337.search-card.all.click

1、Promise

1. 回调地狱

多层回调函数的相互嵌套,就形成了回调地狱。示例代码如下:

setTimeout(() => { // 第 1 称回调函数
  console.log('延时 1s 后输出')

  setTimeout(() => { // 第 2 称回调函数
    console.log('延时 2s 后输出')

    setTimeout(() => { // 第 3 称回调函数
      console.log('延时 3s 后输出')

    }, 3000)

  }, 2000)

}, 1000)


/* 
  最终的打印结果:
  延时 1s 后输出
  延时 2s 后输出
  延时 3s 后输出
*/
回调地狱的缺点:
  1. 代码耦合性太强,牵一发而动全身,难以维护
  2. 大量冗余的代码相互嵌套,代码的可读性变差

1.1 如何解决回调地狱的问题

为了解决回调地狱的问题, ES6 (ECMAScript 2015)中新增了 Promise 的概念。

1.2 Promise 的基本概念

① Promise 是一个构造函数
  • 我们可以创建 Promise 的实例 const p = new Promise()
  • new 出来的 Promise 实例对象,代表一个异步操作
② Promise.prototype 上包含一个 .then() 方法
  • 每一次 new Promise() 构造函数得到的实例对象, 都可以通过原型链的方式访问到 .then() 方法,例如 p.then()
③ .then() 方法用来预先指定成功和失败的回调函数
  • p.then(成功的回调函数失败的回调函数)
  • p.then(result => { }, error => { })
  • 调用 .then() 方法时,成功的回调函数是必选的、失败的回调函数是可选的

代码示例:

 let Pro = new Promise((resoive, reject) => {
      try {
        let res = '成功啦'
        resoive(res) // 把成功信息暴露出去
      }
      catch (e) {
        reject(e) // 把失败信息暴露出去
      }
    })
    Pro.then(
      (res) => {
        console.log('成功', res)
      },
      (e) => {
        console.log('失败', e)
      })

2.基于 then-fs 读取文件内容

安装 then-fs 这个第三方包,从而支持我们基于 Promise 的方式读取文件的内容:
npm i then-fs

素材:

创建: files 文件夹 ,里面存放 1.txt ,2.txt ,3.txt

1.txt  内容: 111  2.txt  内容: 222    3.txt  内容:3333

2.1 then-fs 的基本使用

调用 then-fs 提供的 readFile() 方法,可以异步地读取文件的内容, 它的返回值是 Promise 的实例对象 。因此可以 调用 .then() 方法 为每个 Promise 异步操作指定 成功 失败 之后的回调函数。示例代码如下:
/*
  基于 Promise 的方式,读取文件数据
*/
import thenFs from 'then-fs'

import thenFs from 'then-fs'

thenFs.readFile('./files/1.txt', 'utf8') // 返回值是 Promise 的实例对象
  .then(
    r1 => { console.log(r1) },
    err1 => { console.log(err1.message) }
  )
thenFs.readFile('./files/2.txt', 'utf8')
  .then(
    (r2) => { console.log(r2) },
    err2 => { console.log(err2.message) }
  )
thenFs.readFile('./files/3.txt', 'utf8')
  .then(
    (r3) => { console.log(r3) },
    err3 => { console.log(err3.message) }
  )
注意:上述的代码 无法保证文件的读取顺序 ,需要做进一步的改进!

2.2 .then() 方法的特性

如果上一个 .then() 方法中 返回了一个新的 Promise 实例对象 ,则可以通过下一个 .then() 继续进行处理。通过 .then() 方法的 链式调用 ,就解决了回调地狱的问题

2.3 基于 Promise 按顺序读取文件的内容

Promise 支持链式调用 ,从而来解决回调地狱的问题。示例代码如下:
import thenFs from 'then-fs'

thenFs.readFile('./files/1.txt', 'utf8') // 1.返回值是 Promise 的实例对象
  .then((r1) => { // 2.通过 .then 为第一个 Promise 实例指定成功之后的回调函数
    console.log(r1)
    return thenFs.readFile('./files/2.txt', 'utf8') // 3. 在第一个 .then 中返回一个新的 Promise 实例对象
  })
  .then((r2) => { // 4.继续调用 .then 为上一个 .then 的返回值(新的 Promise 实例)指定成功之后的回调函数
    console.log(r2)
    return thenFs.readFile('./files/3.txt', 'utf8') // 5. 在第二个 .then 中再返回一个新的 Promise 实例对象
  })
  .then((r3) => { // 6.继续调用 .then 为上一个 .then 的返回值(新的 Promise 实例)指定成功之后的回调函数
    console.log(r3)
  })

2.4 通过 .catch 捕获错误

在 Promise 的链式操作中如果发生了错误,可以使用 Promise.prototype. catch 方法进行捕获和处理:
import thenFs from 'then-fs'

thenFs.readFile('./files/11.txt', 'utf8')  // 文件不存在导致读取失败,后面的3个 .then 都不执行
  .then((r1) => { 
    console.log(r1)
    return thenFs.readFile('./files/2.txt', 'utf8') 
  })
  .then((r2) => { 
    console.log(r2)
    return thenFs.readFile('./files/3.txt', 'utf8') 
  })
  .then((r3) => {
    console.log(r3)
  }) 
  .catch((err) => {  // 捕获第 2 行 发生的错误,并输出错误信息
    console.log(err.message) // ENOENT: no such file or directory ....(你的文件路径)
  })
如果不希望前面的错误导致后续的 .then 无法正常执行,则 可以将 .catch 的调用提前 ,示例代码如下:
import thenFs from 'then-fs'

thenFs.readFile('./files/11.txt', 'utf8') 
  .catch((err) => {
    console.log(err.message) // ENOENT: no such file or directory, open + 你的文件路径
  })
  .then((r1) => { 
    console.log(r1) // 11.text 文件不存在所以这里打印 undefined
    return thenFs.readFile('./files/2.txt', 'utf8') 
  })
  .then((r2) => { 
    console.log(r2) // 222
    return thenFs.readFile('./files/3.txt', 'utf8') 
  })
  .then((r3) => { 
    console.log(r3) // 333
  })

2.5 Promise.all() 方法

Promise.all() 方法会发起并行的 Promise 异步操作,等 所有的异步操作全部结束后 才会执行下一步的 .then 操作(等待机制)。示例代码如下
import thenFs from 'then-fs'

// 1.定义一个数组,存放 3 个文件的异步操作
const promiseArr = [
  thenFs.readFile('./files/11.txt', 'utf8'),
  thenFs.readFile('./files/2.txt', 'utf8'),
  thenFs.readFile('./files/3.txt', 'utf8')
]

// 2.将 Promise 的数组,作为 Promise.all() 的参数
Promise.all(promiseArr)
  .then(([r1, r2, r3]) => { // 2.1 所有文件读取成功(等待机制)
    console.log(r1, r2, r3)
  })
  .catch(err => { // 2.2 捕获 Promise 异步操作中的错误
    console.log(err)
  })

2.6 Promise.race() 方法

Promise.race() 方法会发起并行的 Promise 异步操作, 只要任何一个异步操作完成,就立即执行下一步的.then 操作 (赛跑机制)。示例代码如下:
import thenFs from 'then-fs'

// 1.定义一个数组,存放 3 个文件的异步操作
const promiseArr = [
  thenFs.readFile('./files/1.txt', 'utf8'),
  thenFs.readFile('./files/2.txt', 'utf8'),
  thenFs.readFile('./files/3.txt', 'utf8')
]

// 2.将 Promise 的数组,作为 Promise.race() 的参数
Promise.race(promiseArr)
  .then(result=> { // 2.1 只要任何一个异步操作完成,就立即执行下一步的.then 操作
    console.log(result) 
  })
  .catch(err => { // 2.2 捕获 Promise 异步操作中的错误
    console.log(err)
  })

3. 基于 Promise 封装读文件的方法

方法的封装要求:
  1. 方法的名称要定义为 getFile
  2. 方法接收一个形参 fpath,表示要读取的文件的路径
  3. 方法的返回值为 Promise 实例对象

3.1 getFile 方法的基本定义 

// 1.方法的名称为 getFile
// 2.方法接收一个形参 fpath,表示要读取文件的路径
function getFile(fpath) {
  // 3.方法的返回值为 Promise 的实例对象
   return new Promise()
}
注意:第 3 步代码中的 new Promise() 只是创建了一个 形式上的异步操作

3.2 创建具体的异步操作

如果想要创建 具体的异步操作 ,则需要在 new Promise() 构造函数期间, 传递一个 function 函数 将具体的异步操作定义到 function 函数内部 。示例代码如下:
import thenFs from 'then-fs'

// 1.方法的名称为 getFile
// 2.方法接收一个形参 fpath,表示要读取文件的路径
function getFile(fpath) {
  // 3.方法的返回值为 Promise 的实例对象
   return new Promise(()=>{
  // 4. 下面这行代码,表示这是一个具体的、读取文件的异步操作
   thenFs.readFile(fpath, 'utf8', (err, dataStr) => { })
  })
}

3.3 获取 .then 的两个实参

通过 .then() 指定的 成功 失败 的回调函数,可以在 function 的 形参中 进行接收,示例代码如下
import thenFs from 'then-fs'

// 1.方法的名称为 getFile
// 2.方法接收一个形参 fpath,表示要读取文件的路径
function getFile(fpath) {
  // 3.方法的返回值为 Promise 的实例对象
   return new Promise((resolve,reject)=>{
     // 4. 下面这行代码,表示这是一个具体的、读取文件的异步操作
     thenFs.readFile(fpath, 'utf8', (err, dataStr) => { })
  })
}

getFile('./files/11.txt').then(成功的回调函数,失败的回调函数)

getFile 方法的调用过程:

3.4 调用 resolve 和 reject 回调函数

Promise 异步操作的结果 ,可以调用 resolve reject 回调函数进行处理。示例代码如下:
import thenFs from 'then-fs'

function getFile(fpath) {
  return new Promise(function (resolve, reject) {
    thenFs.readFile(fpath, 'utf8', (err, dataStr) => {
      if (err) return reject(err) // 如果读取失败,则调用“失败的回调函数”
      resolve(dataStr)            // 如果读取成功,则调用“成功的回调函数”
    })
  })
}

getFile('./files/11.txt').then(成功的回调函数,失败的回调函数)

到这里基于 Promise 封装读文件的方法就写好了,测试一下

import thenFs from 'then-fs'

function getFile(fpath) {
  return new Promise(function (resolve, reject) {
    thenFs.readFile(fpath, 'utf8', (err, dataStr) => {
      if (err) return reject(err) // 如果读取失败,则调用“失败的回调函数”
      resolve(dataStr)            // 如果读取成功,则调用“成功的回调函数”
    })
  })
}

// getFile('./files/1.txt').then(成功的回调函数,失败的回调函数)

getFile('./files/1.txt')
  .then((r1) => {
    console.log(r1) // 111
  })
  .catch((err) => console.log(err.message))

这里我推荐使用 .catch 来捕获异常

理由:.catch 可以捕获前面 then 方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch方法,而不使用then方法的第二个参数

2、async/await

1. 什么是 async/await

async/await ES8 (ECMAScript 2017)引入的新语法,用来 简化 Promise 异步操作 。在 async/await 出现之前,开发者只能通过 链式 .then() 的方式 处理 Promise 异步操作。示例代码如下:
import thenFs from 'then-fs'

thenFs.readFile('./files/11.txt', 'utf8')  // 文件不存在导致读取失败,后面的3个 .then 都不执行
  .then((r1) => { 
    console.log(r1)
    return thenFs.readFile('./files/2.txt', 'utf8') 
  })
  .then((r2) => { 
    console.log(r2)
    return thenFs.readFile('./files/3.txt', 'utf8') 
  })
  .then((r3) => {
    console.log(r3)
  }) 
  .catch((err) => {  // 捕获第 2 行 发生的错误,并输出错误信息
    console.log(err.message) // ENOENT: no such file or directory ....(你的文件路径)
  })

2.async/await 的基本使用

使用 async/await 简化 Promise 异步操作的示例代码如下:
import thenFs from 'then-fs'

async function getAllFile() {
  const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
  console.log(r1)
  const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
  console.log(r2)
  const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
  console.log(r3)
}

getAllFile()
3. async/await 的 使用注意事项
  1. 如果在 function 中使用了 await,则 function 必须被 async 修饰
  2. async 方法中第一个 await 之前的代码会同步执行,await 之后的代码会异步执行
import thenFs from 'then-fs'

console.log('A')
async function getAllFile() {
  console.log('B')
  const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
  console.log(r1)
  const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
  console.log(r2)
  const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
  console.log(r3)
  console.log('D')
}

getAllFile()
console.log('C') // 注意这里的打印不在 async 方法中,所以不受 await 影响


/* 
最终的打印:
A
B
C
111
222
333
D
*/

3、EventLoop(事件循环)

1. JavaScript 是单线程的语言

JavaScript 是一门 单线程执行 的编程语言。也就是说,同一时间只能做一件事情。

单线程执行任务队列的问题:
如果 前一个任务非常耗时 ,则后续的任务就不得不一直等待,从而导致 程序假死 的问题。

2. 同步任务和异步任务

为了防止某个 耗时任务 导致 程序假死 的问题,JavaScript 把待执行的任务分为了两类:
同步任务 (synchronous)
  •  又叫做非耗时任务,指的是在主线程上排队执行的那些任务
  • 只有前一个任务执行完毕,才能执行后一个任务
异步任务(asynchronous)
  • 又叫做耗时任务,异步任务由 JavaScript 委托给宿主环境进行执行
  • 当异步任务执行完成后,会通知 JavaScript 主线程执行异步任务的回调函数

3. 同步任务和异步任务的执行过程

① 同步任务由 JavaScript 主线程次序执行
② 异步任务 委托给 宿主环境(浏览器Or Node.js)执行
③ 已完成的异步任务 对应的回调函数 ,会被加入到任务队列中等待执行
④ JavaScript 主线程的 执行栈 被清空后,会读取任务队列中的回调函数,次序执行
⑤ JavaScript 主线程不断重复上面的第 4 步

4. EventLoop 的基本概念

JavaScript 主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行 。这个过程是循环不断的,所以整个的这种运行机制又称为 EventLoop (事件循环)。
栈结构是: 先进后出

4. 结合 EventLoop 分析输出的顺序
import thenFs from 'then-fs'

console.log('A')

thenFs.readFile('./files/1.txt', 'utf8').then(dataStr => {
  console.log('B');
})
setTimeout(() => {
  console.log('C')
}, 0)
console.log('D')
正确的输出结果:ADCB。
其中:
  •     A 和 D 属于同步任务。会根据代码的先后顺序依次被执行
  •    C 和 B 属于异步任务。它们的回调函数会被加入到任务队列中,等待主线程空闲时再执行(JavaScript 主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行

4、宏任务和微任务

1. 什么是宏任务和微任务

JavaScript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:
① 宏任务 (macrotask)
  • 异步 Ajax 请求、
  • setTimeout、setInterval、
  • 文件操作
  • 其它宏任务
② 微任务 (microtask)
  • Promise.then、.catch 和 .finally
  • process.nextTick
  • 其它微任务

2.宏任务和微任务的执行顺序

每一个宏任务执行完之后,都会检查 是否存在待执行的微任务 ,如果有,则执行完所有微任务之后,浏览器渲染,继续执行下一个宏任务。

3. 去银行办业务的场景

① 小云和小腾去银行办业务。首先,需要 取号之后进行排队
    ⚫ 宏任务队列
② 假设当前银行网点只有一个柜员,小云在办理存款业务时, 小腾只能等待
    ⚫ 单线程 ,宏任务 按次序执行
③ 小云办完存款业务后,柜员询问他 是否还想办理其它业务
    ⚫ 当前宏任务执行完, 检查是否有微任务
④ 小云告诉柜员:想要买理财产品、再办个信用卡、最后再兑换点马年纪念币?
    ⚫ 执行微任务,后续 宏任务被推迟
⑤ 小云离开柜台后,柜员开始为小腾办理业务
    ⚫ 所有微任务执行完毕 ,开始 执行下一个宏任务

4. 分析以下代码输出的顺序

正确的输出顺序是:2431
分析:
① 先执行所有的 同步任务
执行第 6 行、第 12 行代码
② 再执行 微任务
执行第 9 行代码
③ 再执行 下一个宏任务
执行第 2 行代码

总结

① 能够知道如何 使用 ES6 的模块化语法
    ⚫ 默认导出与默认导入、按需导出与按需导入
② 能够知道如何 使用 Promise 解决回调地狱问题
    ⚫ promise. then ()、promise. catch ()
③ 能够使用 async/await 简化 Promise 的调用
    ⚫ 方法中用到了 await,则方法需要被 async 修饰
④ 能够说出什么是 EventLoop
   ⚫ EventLoop 示意图
⑤ 能够说出宏任务和微任务的执行顺序
   ⚫ 在执行下一个宏任务之前, 先检查是否有待执行的微任务

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狗蛋的博客之旅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值