ES6模块化与异步编程

420次阅读
没有评论

共计 9958 个字符,预计需要花费 25 分钟才能阅读完成。

ES6 模块化

一、什么是 ES6 模块化

1.回顾: node.js 中如何实现模块化

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

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

模块化的好处:

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

2.前端模块化规范的分类

在 ES6 模块化规范 诞生之前,JavaScript 社区已经尝试并提出了 AMD、CMD、CommonJS 等模块化规范。

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

  • AMD 和 CMD 适用于 浏览器端 的 JavaScript 模块化
  • CommonJS 适用于 服务器端 的 JavaScript 模块化

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

3.什么是 ES6 模块化规范

ES6 模块化规范 是 浏览器端与服务器端 通用的模块化开发规范。他的出现极大的降低了前端开发者的学习成本。

ES6 模块化规范中定义:

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

二、在 node.js 中体验 ES6 模块化

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

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

  1. 确保安装了 v14.15.1 或更高版本的 node.js
  2. 在 package.json 的根节点中添加 “type”:“module” 节点npm init -y 初始化包管理配置文件 — package.json

三、导出与导入

5.ES6 模块化的基本语法

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

  1. 默认导出 与 默认导入
  2. 按需导出 与 按需导入
  3. 直接导入 并 执行 模块中的代码

5.1 默认导出与默认导入

默认导出的语法: export default 默认导出的成员

 let n1 = 10 // 定义模块私有成员 n1
 let n2 = 20 // 定义模块私有成员 n2 (外界访问不到 n2,因为它没有被共享出去)
 function show() {} //定义私有方法
 ​
 export default { // 使用 export default 默认导出语法,向外共享 n1 和 show 两个成员
   n1,
   show
 }

默认导入的语法: import 接收名称 from 模块标识符

 // 从 01_m1.js 中导入 export default 向外共享的成员
 // 并使用 m1 成员进行接收
 import m1 from './01_m1.js'
 ​
 // 打印输出的结果为:
 // { n1: 10, show: [Function: show] }
 console.log(m1)
注意事项
默认导出注意事项

每个模块中,只允许使用唯一的一次 export default,否则会报错

默认导入注意事项

默认导入时的 接收名称 可以任意名称,只要是合法的成员名称即可

5.2 按需导出与按需导入

按需导出的语法: export 按需导出的成员

 // 当前模块为 03_m2.js
 ​
 // 向外按需导出变量 s1
 export let s1 = 'aaa'
 // 向外按需导出变量 s2
 export let s2 = 'ccc'
 //向外按需导出方法 say
 export function say() {}

按需导入的语法:import { s1 } from 模块标识符

 // 导入模块成员
 import { s1, s2, say } from './03_m2.js'
 console.log(s1) // 打印输出 aaa
 console.log(s2) // 打印输出 ccc
 console.log(say) // 打印输出 [Function: say]
注意事项
  1. 每个模块中可以使用 多次 按需导出
  2. 按需 导入的成员名称 必须和 按需导出的名称 保持一致
  3. 按需导入时,可以使用 as 关键字 进行重命名
  4. 按需导入可以和默认导入一起使用

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

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

 // 当前文件模块为 05_m3.js
 ​
 // 在当前模块中执行一个 for 循环操作
 for ( let i = 0; i < 3; i++ ) {
   console.log(i)
 }
 //----------------------------
 ​
 // 直接导入并执行模块代码,不需要得到模块向外共享的成员
 import './05_m3.js'

Promise

一、回调地狱以及 Promise 的作用

1.回调地狱

多层回调函数的相互嵌套,就形成了 回调地狱:

 setTimeout(() => {
   console.log('延时 1 秒后输出')
   
   setTimeout(() => {
     console.log('再延时 2 秒后输出')
     
     setTimeout(() => {
       console.log('再延时 3 秒后输出')
     },3000)
   },2000)
 },1000)

回调地狱的缺点:

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

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

为了解决回调地狱的问题,ES6 中新增了 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() 方法时,成功的回调函数是必选的,失败的回调函数是可选的

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

 // 读取文件 1.txt
 fs.readFile('./files/1.txt', 'utf8', (err1, r1) => {
   if (err1) return console.log(err1.message) // 读取文件 1 失败
   console.log(r1) // 读取文件 1 成功
   // 读取文件 2.txt
   fs.readFile('./files/2.txt', 'utf8', (err2, r2) => {
     if (err2) return console.log(err2.message) // 读取文件 2 失败
     console.log(r2) // 读取文件 2 成功
     // 读取文件 3.txt
     fs.readFile('./files/3.txt', 'utf8', (err3, r3) => {
       if (err3) return console.log(err3.message) // 读取文件 3 失败
       console.log(r3) // 读取文件 3 成功
     })
   })
 })

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

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

 npm install then-fs

3.1 then-fs 的基本使用

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

//  基于 Promise 的方式读取文件
import thenFs from 'then-fs'
// 注意:.then() 中的失败回调是可选的,可以被省略
thenFs.readFile('./files/1.txt', 'utf-8').then(r1 => { console.log(r1) }, err1 => {console.log(err1.message) })
thenFs.readFile('./files/2.txt', 'utf-8').then(r2 => { console.log(r1) }, err2 => {console.log(err2.message) })
thenFs.readFile('./files/3.txt', 'utf-8').then(r3 => { console.log(r1) }, err3 => {console.log(err3.message) })

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

3.2 .then() 方法的特性

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

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

Promise 支持链式调用,从而来解决回调地狱的问题:

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)
})

3.4 通过 .catch 捕获错误

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

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 => {  // 捕获第一行发生的错误,并输出错误的消息
    console.log(err.message)
  })

如果不希望前面的错误导致后续的 .then 无法正常执行,则 可以将 .catch 的调用提前:

thenFs.readFile('.files/11.txt', 'utf8')
  .catch(err => { // 捕获第一行发生的错误,并输出错误的信息
    console.log(err.message) // 由于错误已被及时处理,不影响后续 .then 的正常执行
  })
  .then(r1 => { // 输出 undefined
    console.log(r1)
    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
  })

3.5 Promise.all() 方法

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

// 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]) => {
    console.log(r1, r2, r3)
  })
  .catch(err => {
    console.log(err.message)
})

注意:数组中 Promise 实例的顺序,就是最终结果的顺序

3.6 Promise.race() 方法

Promise.race() 方法会发起并行的 Promise 异步操作,只要任何一个异步操作完成,就立即执行下一步的 .then 操作 (赛跑机制)(读取其中一个文件):

// 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.race() 的参数
Promise.race(promiseArr)
  .then((result) => { // 只要任何一个异步操作完成,就立即执行成功的回调函数
    console.log(result)
  })
  .catch((err) => { // 捕获 Promise 异步操作中的错误
    console.log(err.message)
  })

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

方法的封装要求:

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

4.1 getFile 方法的基本定义

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

4.2 创建具体的异步操作

如果想要创建 具体的异步操作,则需要在 new Promise() 构造函数期间,传递一个 function 函数,将具体的异步操作定义到 function 函数内部:

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

4.3 获取 .then 的两个实参

通过 .then() 指定的 成功 和 失败 的回调参数,可以在 function 的 形参中 进行接收:

function getFile(fpath) {
  // resolve 形参是调用 getFiles() 方法时,通过 .then 指定的 成功的 回调函数
  // reject  形参是调用 getFiles() 方法时,通过 .then 指定的 失败的 回调函数
  return new Promise(function(resolve, reject) {
    fs.readFile(fpath, 'utf8', (err, dataStr) => {})
  })
}

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

4.4 调用 resolve 和 reject 回调函数

Promise 异步操作的结果,可以调用 resolve 或 reject 回调函数进行处理:

function getFile(fpath) {
  // resolve 形参是调用 getFiles() 方法时,通过 .then 指定的 成功的 回调函数
  // reject  形参是调用 getFiles() 方法时,通过 .then 指定的 失败的 回调函数
  return new Promise(function(resolve, reject) {
    fs.readFile(fpath, 'utf8', (err, dataStr) => {
      if(err) return reject(err) // 如果读取失败,则调用 失败的回调函数
      resolve(dataStr) // 如果读取成功,则调用 成功的回调函数
    })
  })
}

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

async / await

一、了解 async 和 await 的基本使用

1. 什么是 async / await

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

thenFs.readFile('.files/1.txt', 'utf8')
  .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)
  })

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

缺点:代码冗余、阅读性差、不易理解

2. async / await 的基本使用

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

import thenFs from 'then-fs'

// 按照顺序读取文件 1,2,3 的内容
async function getAllFile() {
  //若某个方法的返回值为Promise实例对象,就可用 await 修饰,修饰后就不为Promise实例了
  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. 在 acync 方法中,第一个 await 之前的代码会同步执行,await 之后的代码会异步执行
console.log('A')
async function getAllFile() {
  console.log('B')
  const r1 = await thenFs.readFile('.files/1.txt', 'utf8')
  const r2 = await thenFs.readFile('.files/2.txt', 'utf8')
  const r3 = await thenFs.readFile('.files/3.txt', 'utf8')
  console.log(r1, r2, r3)
  console.log('D')
}

getAllFile()
console.log('C')
// 最终输出顺序
A
B
C
111 222 333
D

EventLoop

一、同步任务和异步任务

1. JavaScript 是单线程语言

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

单线程执行任务队列的问题:

如果 前一个任务非常耗时,则后续的仍无就不得不一直等待,从而导致 程序假死 的问题。

2. 同步任务和异步任务

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

  1. 同步任务
    • 又叫做 非耗时任务,指的是在主线程上排队执行的那些任务
    • 只有前一个任务执行完毕,才能执行后一个任务
  2. 异步任务
    • 又叫做 耗时任务,异步任务由 JavaScript 委托给 宿主环境进行执行
    • 当异步任务执行完成后,会 通知 JavaScript 主线程 执行异步任务的 回调函数

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

ES6模块化与异步编程
  1. 同步任务由 JavaScript 主线程次序执行
  2. 异步任务 委托 给宿主环境执行
  3. 已完成的异步任务 对应的回调函数,会被加入到任务队列中等待执行
  4. JavaScript 主线程的 执行栈 被清空后,会读取任务队列中的回调函数,次序执行
  5. JavaScript主线程不断重复上面的第四步

4. EventLoop 的基本概念

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

结合 EventLoop 分析输出的顺序

import thenFs from 'then-fs'
console.log('A')
thenFs.readFile('./files/1.txt', 'utf-8').then(dataStr => {
  console.log('B')
})
setTimeout(() => {
  console.log('C')
}, 0)
console.log('D')
// 输出顺序 ADCB
  • A 和 D 属于 同步任务,会根据代码的先后顺序 依次被执行
  • C 和 B 属于 异步任务,他们的回调函数会被加入到任务队列中,等待主线程空闲时再执行

宏任务和微任务

一、宏任务与微任务的概念

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

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

  1. 宏任务
    • 异步 Ajax 请求
    • setTimeout、setInterval
    • 文件操作
    • 其他宏任务
  2. 微任务
    • Promise.then、.catch 和 .finally
    • process.nextTick
    • 其他微任务
ES6模块化与异步编程

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

ES6模块化与异步编程

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

二、经典面试题

分析以下代码输出顺序

1. 分析以下代码输出顺序

setTimeout(function () { // 宏任务
  console.log('1')
})

new Promise(function (resolve) { // 5-7 同步任务
  console.log('2')
  resolve()
}).then(function () { // 8-10 微任务
  console.log('3')
})

console.log('4') // 同步任务
// 输出顺序 2431 
  1. 先执行所有的 同步任务
    • 执行第 6 行、第 12 行代码
  2. 再执行 微任务
    • 执行第 9 行代码
  3. 再执行 下一个宏任务
    • 执行第 2 行代码

2.分析以下代码输出顺序

console.log('1') // 同步任务 ①
setTimeout(function () { // 2-10 宏任务
  console.log('2') //
  new Promise(function (resolve) {
    console.log('3') // ②
    resolve()
  }).then(function () {
    console.log('4') //
  })
})
//-----------------------------
new Promise(function (resolve) {
  console.log('5') // 同步任务
  resolve()
}).then(function () { // 微任务
  console.log('6') //
})
//----------------------------
setTimeout(function () { // 19-27 宏任务
  console.log('7') //
  new Promise(function (resolve) {
    console.log('8') //
    resolve()
  }).then(function () {
    console.log('9') //
  })
})
// 输出顺序 156234789
正文完
 0
Yichend
版权声明:本文于2022-09-24转载自bilibili黑马程序员,共计9958字。
转载提示:此文章非本站原创文章,若需转载请联系原作者获得转载授权。
评论(没有评论)