ES6 模块化

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

:::

ES6 模块化规范中定义:

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

ES6 的模块化3种用法

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

1
2
3
4
5
6
7
8
9
10
11
12
//01_test.js
// 定义成员和方法
let n1 = 10
let n2 = 20
function show(){}

// 默认导出
export default{
n1,
n2,
show
}

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

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

1
2
3
4
// 从 01_test.js 模块导入共享的成员
import test from './01_test.js'
// 输出 { n1:10,n2:20,show:[funtion:show] }
console.log(test)

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


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

1
2
3
4
5
6
7
// 02_test.js
// 向外按需导出变量 s1
export let s1 = 'abc'
// 向外按需导出变量 s2
export let s1 = 123
// 向外按需导出方法 say
export function say() {}

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

1
2
3
4
5
6
7
8
// 导入 02_test.js 模块成员
import {s1,s2,say} from './02test.js'
// 输出 abc
console.log(s1)
// 输出 123
console.log(s2)
// 输出 [funtion:say]
console.log(say)

::: warning 注意事项

:::

  1. 每个模块中可以使用多次按需导出
  2. 按需导入的成员名称必须和按需导出的名称保持一致
  3. 按需导入时,可以使用 as 关键字进行重命名
  4. 按需导入可以和默认导入一起使用

::: tip

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

:::

1
2
3
4
5
6
// 03_test.js

// 执行一个循环
for(let i = 0;i<3;i++){
console.log(i)
}
1
2
3
4
5
// 导入
import './03_test.js'

// 直接运行此文件
// 输出 0,1,2,3,....10

Promise

回调地狱

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setTimeout(() => {

console.log("延迟1秒");

setTimeout(() => {

console.log("延迟2秒");

setTimeout(() => {

console.log("延迟2秒");

}, 3000);

}, 2000);

}, 1000);

回调地狱的缺点:

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

::: tip

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

:::

Promise 的基本概念

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import fs from "fs";
// 读文件 1.txt
fs.readFile("./files/1.txt", "utf8", (err, r1) => {
if (err) return console.log(err.message);
console.log(r1);
// 读文件 2.txt
fs.readFile("./files/2.txt", "utf8", (err, r2) => {
if (err) return console.log(err.message);
console.log(r2);
// 读文件 3.txt
fs.readFile("./files/3.txt", "utf8", (err, r3) => {
if (err) return console.log(err.message);
console.log(r3);
});
});
});

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import thenFs from 'then-fs'
// Promise 支持链式调用,从而来解决回调地狱的问题
thenFs
// 返回值是 promise 的实列对象
.readFile("./files/1.txt", "utf8")
// 通过 .then 为第一个 promise 实例对象指定成功后的回调函数
.then((r1) => {
console.log(r1);
// 在第一个 .then 中返回一个新的 promise 对象
return thenFs.readFile("./files/2.txt", "utf8");
})
// 继续调用 .then ,为上一个 .then 的返回值 (新的实例对象)
// 指定成功后的回调函数
.then((r2) => {
console.log(r2);
return thenFs.readFile("./files/3.txt", "utf8");
})
// 同理 继续调用 .then ,为上一个 .then 的返回值 (新的实例对象)
.then((r3) => {
console.log(r3);
})
// 通过 .catch 捕获错误
// 前面的错误导致后续的 .then 无法正常执行
// 可以将.catch 的调用提前
.catch((err) => {
console.log(err.message);
});

Promise.all() 与 Promise.race() 方法

Promsie 方法

::: tip

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

:::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import thenFs from "then-fs";
// 定义一个数组 存放异步读文件操作
const promiseArr = [
thenFs.readFile("./files/1.txt", "utf8"),
thenFs.readFile("./files/2.txt", "utf8"),
thenFs.readFile("./files/3.txt", "utf8"),
];
// 将数组 作为 promise.all() 参数
Promise.all(promiseArr)
.then(([r1, r2, r3]) => {
// 所有文件读取成功 (等待机制)
// 输出 r1 ,r2 ,r3 文件的内容
console.log(r1, r2, r3);
})
.catch((err) => {
console.log(err.message);
});

// 数组中 Promise 实例的顺序,
// 就是最终结果的顺序!

::: tip

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

:::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import thenFs from "then-fs";
// 定义一个数组 存放异步读文件操作
const promiseArr = [
thenFs.readFile("./files/1.txt", "utf8"),
thenFs.readFile("./files/2.txt", "utf8"),
thenFs.readFile("./files/3.txt", "utf8"),
];
// 将数组 作为 promise.race() 参数
Promise.race(promiseArr)
.then((results) => {
// 只要任何一个异步操作完成 就执行回调函数(赛跑机制)
// 例如:文件2.txt 先读取完
// 就输出 2.txt 的文件内容 结束
console.log(results);
})
.catch((err) => {
console.log(err.message);
});

async/await

::: tip async/await 是 ES8(ECMAScript 2017)引入的新语法,用来简化 Promise 异步操作

:::

  • .then 链式调用的优点:解决了回调地狱的问题
  • .then 链式调用的缺点:代码冗余、阅读性差、不易理解

async/await 简化 Promise 异步操作的使用

1
2
3
4
5
6
7
8
9
10
11
12
import thenFs from "then-fs";
// 按顺序读取文件 1,2,3 的内容
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();

::: warning async/await 的使用注意事项

:::

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

EventLoop(事件循环)

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

事件循环

结合 EventLoop 分析输出的顺序

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

console.log('A');
thenFs.readFile("./files/3.txt", "utf8").then(dataStr=>{
console.log('B');
})
setTimeout(()=>{
console.log('C');
},0)
console.log('D');

::: details 查看答案

ADCB

  • A 和 D 属于同步任务。会根据代码的先后顺序依次被执行
  • C 和 B 属于异步任务。它们的回调函数会被加入到任务队列中,等待主线程空闲时再执行

:::

宏任务和微任务

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

  • 宏任务(macrotask)
    • 异步 Ajax 请求、
    • setTimeout、setInterval、
    • 文件操作
    • 其它宏任务
  • 微任务(microtask)
    • Promise.then、.catch 和 .finally
    • process.nextTick
    • 其它微任务

宏任务和微任务

::: tip 宏任务和微任务的执行顺序

:::

执行顺序

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

::: tip 宏任务和微任务日常例子

:::

去银行办业务的场景

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

::: tip 宏任务和微任务练习例子

:::

1
2
3
4
5
6
7
8
9
10
setTimeout(function () {
console.log(1);
});
new Promise(function (resolve) {
console.log(2);
resolve();
}).then(function () {
console.log(3);
});
console.log(4);

::: details 查看答案

2431

分析:

  1. 先执行所有的同步任务

    • 第 6 行 ,第 12 行

      注:promise 是同步 ,promise.then 回调才是异步

  2. 在执行所有的微任务

    • 第9行
  3. 再执行下一个宏任务

    • 第2行

:::