08 ES6模块化与异步编程高级用法
1. ES6 模块化
1.1 node.js 的模块化
node.js
遵循了 CommonJS
的模块化规范。其中:
- 导入其它模块使用
require()
方法 - 模块对外共享成员使用
module.exports
对象
模块化的好处:大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用
1.2 前端模块化规范的分类
在 ES6 模块化规范诞生之前,JS 社区已经尝试并提出了 AMD
、CMD
、CommonJS
等模块化规范。
但是,这些由社区提出的模块化标准,还是存在一定的差异性与局限性、并不是浏览器与服务器通用的模块化标准,例如:
AMD
和CMD
适用于浏览器端的 JS 模块化CommonJS
适用于服务器端的 JS 模块化
太多的模块化规范给开发者增加了学习的难度与开发的成本。因此,大一统的 ES6 模块化规范诞生了!
1.3 什么是 ES6 模块化规范
ES6 模块化规范是浏览器端与服务器端通用的模块化开发规范。它的出现极大的降低了前端开发者的模块化学习成本,开发者不需再额外学习 AMD
、CMD
或 CommonJS
等模块化规范。
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 同步任务和异步任务的执行过程
- 同步任务由 JS 主线程次序执行
- 异步任务委托给宿主环境执行
- 已完成的异步任务对应的回调函数,会被加入到任务队列中等待执行
- JS 主线程的执行栈被清空后,会读取任务队列中的回调函数,次序执行
- JS 主线程不断重复上一步
4.4 EventLoop 的基本概念
JS 主线程从 “任务队列” 中读取异步任务的回调函数,放到执行栈中依次执行。这个过程是循环不断的,所以整个的这种运行机制又称为 EventLoop
(事件循环)。
4.5 结合 EventLoop 分析输出的顺序
结果应该是:ADCB
- A 和 D 是同步任务,B 和 C 是异步任务
- 同步任务先执行,所以先打印A,再打印 D
- 异步任务在没有同步任务的时候执行,由于定时器延迟为0,所以 C 先于 B 放入任务队列,所以先 C 再 B
5. 宏任务和微任务
5.1 什么是宏任务和微任务
JS 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:
-
宏任务(macrotask)
- 异步
Ajax
请求 setTimeout
、setInterval
- 文件操作
- 其它宏任务
- 异步
-
微任务(microtask)
Promise.then
、.catch
和.finally
process.nextTick
- 其它微任务
5.2 宏任务和微任务的执行顺序
每一个宏任务执行完之后,都会检查是否存在待执行的微任务,如果有,则执行完所有微任务之后,再继续执行下一个宏任务。
5.3 分析以下代码输出的顺序
结果是:2431
分析:
- 先执行所有的同步任务:执行第 6 行、第 12 行代码
- 再执行微任务:执行第 9 行代码
- 再执行下一个宏任务:执行第 2 行代码
5.4 经典面试题
请分析以下代码输出的顺序(代码较长,截取成了左中右 3 个部分) :
正确的输出顺序是:156234789
一开始有两个同步任务:1 和 5
另外有两个宏任务一个微任务,先执行微任务 6
两个宏任务,先执行左边的(代码顺序),左边的里面:2,3是同步任务,4 是微任务,所以顺序为2,3,4。
另外一个宏任务同理可得,7,8,9。
如有错误,敬请指正,欢迎交流🤝,谢谢♪(・ω・)ノ
转载自:https://juejin.cn/post/7267470184242675752