likes
comments
collection
share

08 ES6模块化与异步编程高级用法

作者站长头像
站长
· 阅读数 77

1. ES6 模块化

1.1 node.js 的模块化

node.js 遵循了 CommonJS 的模块化规范。其中:

  • 导入其它模块使用 require() 方法
  • 模块对外共享成员使用 module.exports 对象

模块化的好处:大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用

1.2 前端模块化规范的分类

在 ES6 模块化规范诞生之前,JS 社区已经尝试并提出了 AMDCMDCommonJS 等模块化规范。

但是,这些由社区提出的模块化标准,还是存在一定的差异性与局限性、并不是浏览器与服务器通用的模块化标准,例如:

  • AMDCMD 适用于浏览器端的 JS 模块化
  • CommonJS 适用于服务器端的 JS 模块化

太多的模块化规范给开发者增加了学习的难度与开发的成本。因此,大一统的 ES6 模块化规范诞生了!

1.3 什么是 ES6 模块化规范

ES6 模块化规范是浏览器端与服务器端通用的模块化开发规范。它的出现极大的降低了前端开发者的模块化学习成本,开发者不需再额外学习 AMDCMDCommonJS 等模块化规范。

ES6 模块化规范中定义:

  • 每个 js 文件都是一个独立的模块
  • 导入其它模块成员使用 import 关键字
  • 向外共享模块成员使用 export 关键字

1.4 在 node.js 中体验 ES6 模块化

node.js 中默认仅支持 CommonJS 模块化规范,若想基于 node.js 体验与学习 ES6 的模块化语法,可以按照如下两个步骤进行配置:

  • 确保安装了 v14.15.1 或更高版本的 node.js
  • package.json 的根节点中添加 "type": "module" 节点

1.5 ES6 模块化的基本语法

ES6 的模块化主要包含如下 3 种用法:

  • 默认导出与默认导入

  • 按需导出与按需导入

  • 直接导入并执行模块中的代码

1.5.1 默认导出与默认导入

1. 默认导出

语法:

export default 默认导出的成员

例如:

let n1 = 1 // 定义模块私有成员 n1
let n2 = 2 // 定义模块私有成员 n2, 外界无法访问:因为没有向外导出

// 定义模块私有方法
function sayHi() {
  console.log('Hi')
}

export default {
  n1,
  sayHi,
}

2. 默认导入

语法:

 import 接收名称 from '模块标识符'

例如:

import m1 from './default.js'
console.log(m1) // { n1: 1, sayHi: [Function: sayHi] }

// 使用模块成员
console.log(m1.n1) // 1

// 使用模块方法
m1.sayHi() // Hi

3. 注意事项

  • 默认导出:每个模块中,只允许使用唯一的一次 export default,否则会报错
  • 默认导入:接收名称可以任意名称,只要是合法的成员名称即可

1.5.2 按需导出与按需导入

1. 按需导出

语法:

export 按需导出的成员

例如:

// 向外按需导出变量 n3
export let n3 = 3
// 向外按需导出变量 n4
export let n4 = 4
// 向外按需导出方法 sayHello
export const sayHello = () => {
  console.log('Hello')
}

2. 按需导入

语法:

import { 模块定义的成员或方法 } from '模块标识符'

例如:

// 导出模块里的成员 n3, n4, 方法 sayHello
import { n3, n4, sayHello } from './required.js'
console.log(n3) // 3
console.log(n4) // 4
sayHello() // Hello

3. 注意事项

  • 按需导出:每个模块中可以使用多次按需导出

  • 按需导入:

    • 按需导入成员名称必须和按需导出的名称保持一致

    • 按需导入时,可以使用 as 关键字进行重命名,重命名之后,不能再使用原来的变量名或函数名

      import { n3 as m1, n4, sayHello } from './required.js'
      console.log(m1) // 3
      console.log(n3) // 报错: n3 is not defined
      
    • 按需导入可以和默认导入一起使用

1.5.3 直接导入并执行模块中的代码

如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。此时,可以直接导入并执行模块代码,示例如下:

模块文件 use.js

console.log('use1')
console.log('use2')

主文件 index.js

import './use.js'
// 结果打印出 use1 和 use2

2. Promise

2.1 回调地狱

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

setTimeout(() => {
  console.log('延迟 1s 后输出')

  setTimeout(() => {
    console.log('再延迟 2s 后输出')

    setTimeout(() => {
      console.log('再延迟 3s 后输出')
    }, 3000)
  }, 2000)
}, 1000)

回调地狱的缺点:

  • 代码耦合性太强,牵一发而动全身,难以维护
  • 大量冗余的代码相互嵌套,代码的可读性变差

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

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

2.3 Promise 的基本概念

  • Promise 是一个构造函数

    • 我们可以创建 Promise 的实例 const p = new Promise()
    • new 出来的 Promise 实例对象,代表一个异步操作
  • Promise.prototype 上包含一个 .then() 方法

    • 每一次 new Promise() 构造函数得到的实例对象,都可以通过原型链的方式访问到 .then() 方法
  • .then() 方法用来预先指定成功和失败的回调函数

    p.then(成功的回调函数,失败的回调函数)
    // 即
    p.then(result => { }, error => { })
    
    • 调用 .then() 方法时,成功的回调函数是必选的、失败的回调函数是可选的

2.4 基于回调函数按顺序读取文件内容

import fs from 'fs'
fs.readFile('./file/1.txt', 'utf-8', (err1, r1) => {
  if (err1) return console.log(err1.message)
  console.log(r1)

  fs.readFile('./file/2.txt', 'utf-8', (err2, r2) => {
    if (err2) return console.log(err2.message)
    console.log(r2)

    fs.readFile('./file/3.txt', 'utf-8', (err3, r3) => {
      if (err3) return console.log(err3.message)
      console.log(r3)
    })
  })
})

该代码形成了回调地狱,所以我们需要优化。

2.5 then-fs

由于 node.js 官方提供的 fs 模块仅支持以回调函数的方式读取文件,不支持 Promise 的调用方式。因此,需要先运行如下的命令,安装 then-fs 这个第三方包,从而支持我们基于 Promise 的方式读取文件的内容:

npm i then-fs

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

import thenFs from 'then-fs'
thenFs.readFile('./file/1.txt', 'utf8').then(
  (r1) => {
    console.log(r1)
  },
  (err1) => {
    console.log(err1.message)
  }
)

thenFs.readFile('./file/2.txt', 'utf8').then(
  (r2) => {
    console.log(r2)
  },
  (err2) => {
    console.log(err2.message)
  }
)

thenFs.readFile('./file/3.txt', 'utf8').then(
  (r3) => {
    console.log(r3)
  },
  (err3) => {
    console.log(err3.message)
  }
)

注意:上述的代码无法保证文件的读取顺序,需要做进一步的改进!

2.6 .then() 方法的特性

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

thenFs
  .readFile('./file/1.txt', 'utf8')
  .then((r1) => {
    console.log(r1)
    return thenFs.readFile('./file/2.txt', 'utf8')
  })
  .then((r2) => {
    console.log(r2)
    return thenFs.readFile('./file/3.txt', 'utf8')
  })
  .then((r3) => console.log(r3))

2.7 通过 .catch 捕获错误

Promise 的链式操作中如果发生了错误,可以使用 Promise.prototype.catch 方法进行捕获和处理。

thenFs
  .readFile('./file/1.txt', 'utf8')
  .then((r1) => {
    console.log(r1)
    return thenFs.readFile('./file/2.tdxt', 'utf8')
  })
  .then((r2) => {
    console.log(r2)
    return thenFs.readFile('./file/3.txt', 'utf8')
  })
  .then((r3) => console.log(r3))
  // 加了catch
  .catch((err) => {
    console.log(err)
  })

catch() 加在最后表示一旦该代码某个步骤出现异常,就能捕获到,且异常之后无法执行。如读取文件 1 出现异常,就不会读取文件 1、文件 2 和文件 3 ,读取文件 2 出现异常,就不会读取文件 2 和文件 3 。

如果我只想捕获文件 1 的错误,可以这么写:

thenFs
  .readFile('./file/1.txt', 'utf8')
  .then((r1) => {
    console.log(r1)
    return thenFs.readFile('./file/2.tdxt', 'utf8')
  })
  .catch((err1) => {
    console.log(err1)
  })
  .then((r2) => {
    console.log(r2)
    return thenFs.readFile('./file/3.txt', 'utf8')
  })
  .then((r3) => console.log(r3))

在文件 1 后面使用 catch 即可,如果是其他文件出异常,则不会捕获,如果文件 1 出异常,则捕获之后,文件 1 及其之后文件无法读取。

总结:

  • catch 放最后,代码发生异常,后续代码不执行
  • catch 不放最后,代码发生异常,异常代码不执行,不影响后续代码执行

2.8 Promise.all() 方法

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

// 1. 定义数组
const promiseArr = [
  thenFs.readFile('./file/1.txt', 'utf8'),
  thenFs.readFile('./file/2.txt', 'utf8'),
  thenFs.readFile('./file/3.txt', 'utf8'),
]

// 2. 将 promis 数组做为 promise.all 的参数
Promise.all(promiseArr)
  .then(([r1, r2, r3]) => { // 等待机制,等所有文件读取成功
    console.log(r1, r2, r3)
  })
  .catch((err) => {
    console.log(err.message)
  })

等待机制使得 all 里面所有异步操作执行完,才执行后续步骤,且 then 参数的接收顺序,和定义异步数组顺序一致。

2.9 Promise.race() 方法

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

Promise.race(promiseArr)
  .then((result) => {
    // 等待机制,等所有文件读取成功
    console.log(result)
  })
  .catch((err) => {
    console.log(err.message)
  })

赛跑机制使得运行最快的异步操作执行结束后,不再执行其他异步操作。

3. async/await

3.1 什么是 async/await

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

thenFs
  .readFile('./file/1.txt', 'utf8')
  .then((r1) => {
    console.log(r1)
    return thenFs.readFile('./file/2.txt', 'utf8')
  })
  .then((r2) => {
    console.log(r2)
    return thenFs.readFile('./file/3.txt', 'utf8')
  })
  .then((r3) => console.log(r3))

.then 链式调用的优点:解决了回调地狱的问题

.then 链式调用的缺点:代码冗余、阅读性差、不易理解

3.2 async/await 的基本使用

使用 async/await 简化 Promise 异步操作的示例代码如下:

import thenFs from 'then-fs'

// 按顺序读取文件 1,2,3 的内容
async function getAllFiles() {
  const r1 = await thenFs.readFile('./file/1.txt', 'utf8')
  console.log(r1)
  const r2 = await thenFs.readFile('./file/2.txt', 'utf8')
  console.log(r2)
  const r3 = await thenFs.readFile('./file/3.txt', 'utf8')
  console.log(r3)
}

getAllFiles()

async/await 的使用注意事项:

  • 如果在 function 中使用了 await,则 function 必须被 async 修饰
  • async 方法中,第一个 await 之前的代码会同步执行,await 之后的代码会异步执行

4. EventLoop

4.1 JS 是单线程的语言

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

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

4.2 同步任务和异步任务

为了防止某个耗时任务导致程序假死的问题,JS 把待执行的任务分为了两类:

  • 同步任务(synchronous

    • 又叫做非耗时任务,指的是在主线程上排队执行的那些任务
    • 只有前一个任务执行完毕,才能执行后一个任务
  • 异步任务(asynchronous

    • 又叫做耗时任务,异步任务由 JS 委托给宿主环境进行执行
    • 当异步任务执行完成后,会通知 JS 主线程执行异步任务的回调函数

4.3 同步任务和异步任务的执行过程

08 ES6模块化与异步编程高级用法

  • 同步任务由 JS 主线程次序执行
  • 异步任务委托给宿主环境执行
  • 已完成的异步任务对应的回调函数,会被加入到任务队列中等待执行
  • JS 主线程的执行栈被清空后,会读取任务队列中的回调函数,次序执行
  • JS 主线程不断重复上一步

4.4 EventLoop 的基本概念

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

4.5 结合 EventLoop 分析输出的顺序

08 ES6模块化与异步编程高级用法

结果应该是:ADCB

  • A 和 D 是同步任务,B 和 C 是异步任务
  • 同步任务先执行,所以先打印A,再打印 D
  • 异步任务在没有同步任务的时候执行,由于定时器延迟为0,所以 C 先于 B 放入任务队列,所以先 C 再 B

5. 宏任务和微任务

5.1 什么是宏任务和微任务

JS 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:

  • 宏任务(macrotask

    • 异步 Ajax 请求
    • setTimeoutsetInterval
    • 文件操作
    • 其它宏任务
  • 微任务(microtask

    • Promise.then.catch.finally
    • process.nextTick
    • 其它微任务

08 ES6模块化与异步编程高级用法

5.2 宏任务和微任务的执行顺序

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

5.3 分析以下代码输出的顺序

08 ES6模块化与异步编程高级用法

结果是:2431

分析:

  • 先执行所有的同步任务:执行第 6 行、第 12 行代码
  • 再执行微任务:执行第 9 行代码
  • 再执行下一个宏任务:执行第 2 行代码

5.4 经典面试题

请分析以下代码输出的顺序(代码较长,截取成了左中右 3 个部分) :

08 ES6模块化与异步编程高级用法

正确的输出顺序是:156234789

一开始有两个同步任务:1 和 5

另外有两个宏任务一个微任务,先执行微任务 6

两个宏任务,先执行左边的(代码顺序),左边的里面:2,3是同步任务,4 是微任务,所以顺序为2,3,4。

另外一个宏任务同理可得,7,8,9。


本文主要学习 黑马程序员Vue全套视频教程,从vue2.0到vue3.0一套全覆盖,前端学习核心框架教程

如有错误,敬请指正,欢迎交流🤝,谢谢♪(・ω・)ノ

转载自:https://juejin.cn/post/7267470184242675752
评论
请登录