ES6模块化&Promise&EventLoop

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

ES6模块化

node中如何实现模块化

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

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

前端模块化的分类

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

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

标准,例如:

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

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

ES6模块化规范

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

习成本,开发者不需再额外学习 AMD、CMD 或 CommonJS 等模块化规范。

ES6 模块化规范中定义:

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

node.js 中默认仅支持 CommonJS 模块化规范,若想基于 node.js 体验与学习 ES6 的模块化语法,可以按照

如下两个步骤进行配置:

① 确保安装了 v14.15.1 或更高版本的 node.js

② 在 package.json 的根节点中添加 “type”: “module” 节点

ES6模块化的基本语法

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

① 默认导出与默认导入

② 按需导出与按需导入

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

1.1默认导出

1
export default 默认导出的成员
1
2
3
4
5
6
7
8
let n1 = 10
let n2 = 20
function show() {}

export default {
n1,
show
}
  • 每个模块中,只允许使用唯一的一次 export default,否则会报错!

1.1默认导入

1
import xxx from ''       
1
2
3
import m1 from './01.默认导出.js'

console.log(m1)
  • 默认导入时的接收名称可以任意名称,只要是合法的成员名称即可

2.1按需导出

1
export 按需导出的成员
1
2
3
export let s1 = 'aaa'
export let s2 = 'ccc'
export function say() {}

2.2 按需导入

1
import { s1 } from '模块标识符'
1
import { s1, s2, say } from './03.按需导出.js'

2.3 注意事项

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

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

③ 按需导入时,可以使用 as 关键字进行重命名

④ 按需导入可以和默认导入一起使用

1
import info, { s1, s2 as str2, say } from './03.按需导出.js'

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

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

块代码

Promise

回调地狱

异步任务
与之相对应的概念是“同步任务”,同步任务在主线程上排队执行,只有前一个任务执行完毕,才能执行下一个任务。异步任务不进入主线程,而是进入异步队列,前一个任务是否执行完毕不影响下一个任务的执行。同样,还拿定时器作为异步任务举例:

1
2
3
4
5
setTimeout(function(){
console.log('执行了回调函数');
},3000)
console.log('111');

先输出111

存在异步任务的代码,不能保证能按照顺序执行,那如果我们非要代码顺序执行呢

比如我要说一句话,语序必须是下面这样的:武林要以和为贵,要讲武德,不要搞窝里斗。
我必须要这样操作,才能保证顺序正确:

1
2
3
4
5
6
7
8
9
10
setTimeout(function () {  //第一层
console.log('武林要以和为贵');
setTimeout(function () { //第二程
console.log('要讲武德');
setTimeout(function () { //第三层
console.log('不要搞窝里斗');
}, 1000)
}, 2000)
}, 3000)

可以看到,代码中的回调函数套回调函数,居然套了3层,这种回调函数中嵌套回调函数的情况就叫做回调地狱

回调地狱的缺点:

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

基本概念

Promise 是一个构造函数

  • 我们可以创建 Promise 的实例 const p = new Promise()
  • new 出来的 Promise 实例对象,代表一个异步操作

Promise.prototype 上包含一个 .then() 方法

  • 每一次 new Promise() 构造函数得到的实例对象,
  • 都可以通过原型链的方式访问到 .then() 方法,例如 p.then()

.then() 方法用来预先指定成功和失败的回调函数

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

基于 then-fs 读取文件内容

由于 node.js 官方提供的 fs 模块仅支持以回调函数的方式读取文件,不支持 Promise 的调用方式。因此,需

要先运行如下的命令,安装 then-fs 这个第三方包,从而支持我们基于 Promise 的方式读取文件的内容:

1
npm install then-fs

调用 then-fs 提供的 readFile() 方法,可以异步地读取文件的内容,它的返回值是 Promise 的实例对象。因

此可以调用 .then() 方法为每个 Promise 异步操作指定成功和失败之后的回调函数。

1
2
3
4
5
import thenFs from 'then-fs'

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

但是上述的代码不能保证按顺序执行

.then()方法的特性

如果上一个 .then() 方法中返回了一个新的 Promise 实例对象,则可以通过下一个 .then() 继续进行处理。通

过 .then() 方法的链式调用,就解决了回调地狱的问题

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

Promise 支持链式调用,从而来解决回调地狱的问题。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import thenFs from 'then-fs'

thenFs
.readFile('./files/11.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')
})
.then((r3) => {
console.log(r3)
})

通过.catch捕获错误

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import thenFs from 'then-fs'

thenFs
.readFile('./files/11.txt', 'utf8') //
.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')
})
.then((r3) => {
console.log(r3)
})
.catch((err)=>{
console.log(err)
})

假设文件不存在导致读取失败,后面3个then不执行

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import thenFs from 'then-fs'

thenFs
.readFile('./files/11.txt', 'utf8')
.catch((err) => { //错误已经被处理,不影响后续.then执行
console.log(err.message)
})
.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是异步操作吗,如果在执行前有别的同步操作会怎样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve(5);
console.log(2);
}).then(val => {
console.log(val);
});

promise.then(() => {
console.log(3);
});

console.log(4);

setTimeout(function() {
console.log(6);
});
执行结果124536
1
Promise new的时候会立即执行里面的代码 then是微任务 会在本次任务执行完的时候执行 setTimeout是宏任务 会在下次任务执行的时候执行

resolve("Async operation completed") 是 Promise 中的方法,用于将 Promise 对象的状态从 “pending”(进行中)转变为 “fulfilled”(已完成),同时传递一个结果值(在这里是字符串 “Async operation completed”)。

在上述示例中,resolve("Async operation completed") 是在 Promise 的执行器函数内部执行的,具体来说是在 new Promise((resolve, reject) => {...}) 中的回调函数内部执行的。这个回调函数会在 Promise 对象被创建时立即执行。

当 Promise 的执行器函数内部调用 resolve 方法时,Promise 对象的状态就会变为 “fulfilled”,同时传递的结果值会成为该 Promise 对象的终值(fulfillment value)。

在示例中,resolve("Async operation completed") 是在 Promise 对象创建后的某个时间点(例如,在调用 new Promise(...) 之后立即调用)执行的。这是因为 Promise 的执行器函数是同步执行的(但是这个函数里面如果执行了异步的操作,那么那个操作仍然是异步的,这里只是说这个函数是同步的,但是不能保证里面的操作,可以看下面的那个案例),它会立即执行回调函数,并在回调函数中调用 resolvereject 方法来改变 Promise 的状态。

执行器函数

在Promise中,执行器函数(executor function)是作为参数传递给Promise构造函数的一个函数。执行器函数在创建Promise对象时立即执行,它负责定义异步操作,并通过调用resolve或reject来改变Promise的状态。

执行器函数接受两个参数:resolve和reject。这些参数都是函数,由Promise构造函数提供,并且由JavaScript引擎在Promise对象创建时自动传递给执行器函数。

通常情况下,执行器函数包含异步操作(如网络请求、文件读取等),并在异步操作完成时调用resolve方法来表示操作成功,或调用reject方法来表示操作失败。通过调用这些方法,执行器函数可以决定Promise对象的最终状态和终值。

1
2
3
4
5
6
7
8
9
10
11
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const randomNum = Math.random();
if (randomNum < 0.5) {
resolve("Operation succeeded");
} else {
reject("Operation failed");
}
}, 2000);
});

在上述示例中,Promise的执行器函数包含了一个异步操作(使用setTimeout模拟),在2秒后随机生成一个数,并通过比较来决定调用resolve或reject。如果生成的数小于0.5,则调用resolve表示操作成功,否则调用reject表示操作失败。

通过这种方式,执行器函数可以定义Promise的行为和异步操作的逻辑,以及在异步操作完成后如何改变Promise的状态和传递最终的结果。

Promise.all()方法

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

操作(等待机制)。

1
2
3
4
5
6
7
8
9
10
11
12
13
import thenFs from 'then-fs'

const promiseArr = [
thenFs.readFile('./files/3.txt', 'utf8'),
thenFs.readFile('./files/2.txt', 'utf8'),
thenFs.readFile('./files/1.txt', 'utf8'),
]

Promise.all(promiseArr).then(result => {
console.log(result)
})
//result是包含所以解析值的数组
//返回一个新的Promise对象,该对象在promiseArr中的所有Promise对象都被解析时才被解析,并提供一个包含所有解析值的数组作为回调函数的参数

Promise.race()方法

Promise.race() 方法会发起并行的 Promise 异步操作,只要任何一个异步操作完成,就立即执行下一步的

.then 操作(赛跑机制)。

1
2
3
4
5
6
7
8
9
10
11
import thenFs from 'then-fs'

const promiseArr = [
thenFs.readFile('./files/3.txt', 'utf8'),
thenFs.readFile('./files/2.txt', 'utf8'),
thenFs.readFile('./files/1.txt', 'utf8'),
]

Promise.race(promiseArr).then(result => {
console.log(result)
})

调用Promise.race(promiseArr),返回一个新的Promise对象。这个新的Promise对象将会在promiseArr中最快完成的Promise对象被解析时被解析

基于Promise封装读文件方法

① 方法的名称要定义为 getFile

② 方法接收一个形参 fpath,表示要读取的文件的路径

③ 方法的返回值为 Promise 实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import fs from 'fs'

function getFile(fpath) {
return new Promise(function (resolve, reject) {
fs.readFile(fpath, 'utf8', (err, dataStr) => {
if (err) return reject(err)
resolve(dataStr)
})
})
}

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

一般直接使用.catch()做错误处理而不用then中第二个参数

async/await

概念

(语法糖)

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

注意事项

① 如果在 function 中使用了 await,则 function 必须被 async 修饰

② 在 async 方法中,第一个 await 之前的代码会同步执行,await 之后的代码会异步执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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')

(await相当于.then()方法,thenFs.readFile(‘./files/1.txt’, ‘utf8’)这个promise把终值给了await)

EventLoop

可以参考这一次,彻底弄懂 JavaScript 执行机制 - 掘金 (juejin.cn)

JS是单线程执行的编程语言

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

① 同步任务(synchronous)

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

② 异步任务(asynchronous)

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

(图来自黑马程序员)

任务队列根据宏任务和微任务有对应的宏任务队列和微任务队列

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

宏任务和微任务

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

① 宏任务(macrotask)

  • 整体代码script

  • 异步 Ajax 请求、

  • setTimeout、setInterval、

  • 文件操作

  • I/O

  • UI交互事件(比如DOM)

  • 其它宏任务

② 微任务(microtask)

  • Promise.then、.catch 和 .finally

  • process.nextTick(Node独有,注册函数的优先级比Promise回调函数要高)

    因为node中的event loop和浏览器中的event loop运行方式不一样。
    浏览器中的机制就像作者说的那样:一个宏,所有微;再一个宏,再所有微 这样的去执行事件循环(也可以理解为微任务在宏任务之间的间隙去执行)。
    而node中,是一类一系列这样子去执行的。是一类宏,然后本次循环所有微;再一类宏,所有微 这样子的。
    然后题中两个setTimeout属于一类,即使他们每个宏中又各自有微,也是先执行完这一类所有setTimeout之后才执行本次剩下的微任务的。所以是这个结果。当然,node中nextTick的执行优先级高于then的。

    (高版本的node就和浏览器一样了)

  • 其它微任务

引入了这两个概念之后,事件循环就变成这样了:

1.整个script代码作为第一个宏任务开始执行,同步任务立即执行,遇到宏任务后推到宏任务队列,遇到微任务推到微任务队列,

2.当第一次事件循环中的执行栈为空时,就去执行微任务队列中的事件,微任务队列为空时开始第二次事件循环,

3.第二次事件循环会将宏任务队列中最顶端的事件加入到执行栈中执行,执行过程中遇到微任务则推入到微任务队列中,如果遇到宏任务则把宏任务推入到宏任务队列中,当执行栈为空时,再去执行微任务队列中的事件,直到微任务队列为空,重复第3步中的操作开始下一次事件循环,直到所有宏任务,微任务都为空为止


ES6模块化&Promise&EventLoop
https://wjcbolg.cn/2023/06/08/ES6模块化和异步编程高级用法/
作者
JasonWang
发布于
2023年6月8日
许可协议
BY-JW