title: ES6模块化

categories: Javascript
tag: JS
date: 2021-09-27 19:45:34

ES6 模块化

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

ES6 款模块化规范中定义:

  1. 每个 JS 都是独立的模块
  2. 导入其他模块使用 import 关键字
  3. 向外共享其他模块使用 export 关键字

在 nodeJS 使用 ES6 模块化

  • 确保安装了 v14.15.1 或高于的 node 版本
  • 输入npm init -y。在 package.json 里面添加"type": "module"

ES6 模块化语法

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

默认导出与默认导入

export default 默认导出的成员对象

  1. let n1 = 10
  2. let n2 = 20
  3. function show() {}
  4. export default {
  5. n1,
  6. show
  7. }

import 接受名称 from '模块的路径'

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

注意事项

  1. 在每一个模块中,只允许使用一次默认导出。
  2. 默认导入时的接受名称可以任意名称,只要是合法的名称即可

按需导出与按需导入

export 按需导出的成员

  1. export let s1 = 'aaa'
  2. export let s2 = 'ccc'
  3. export function say() {}

import {s1} from '路径'

  1. import { s1, s2, say } from './3.js'
  2. console.log(s1, s2, say)

注意事项

  1. 每个模块中可以使用多次按需导出
  2. 按需导入的名称必须和按需导出的名称保持一致
  3. 按需导入时,可以使用 as 关键字进行重命名
  1. import { s1, s2 as str2, say } from './3.js'
  2. console.log(s1, str2, say)
  1. 按需导入和默认导入可以一起使用

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

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

  1. 在这个4.js文件夹中,我们没有export哦。
  2. for (let i = 0; i < 3; i++) {
  3. console.log(i)
  4. }
  1. import './4.js '

此时运行 3.js 的话,控制台直接打印 0,1,2

Promise

回调地狱

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

缺点:

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

如何解决回调地狱?

ES6 中引入了 Promise 的概念

Promise 的基本概念

  1. Promise 是一个构造函数
  • 我们可以创建 Promise 的实例
    const p=new Promise()
  • new 出来的 Promise 实例对象,代表一个异步操作
  1. Promise.prototype上有一个.then()方法
  2. .then()方法用来预先制定成功和失败的回调函数
  • p.then(成功的回调函数,失败的回调函数)
  • p.then(result=>{},error=>{})
  • 调用.then()方法时,成功的回调函数是必选的,失败的回调函数是可选的

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

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

  1. then-fs 的用法
    调用 then-fs 提供的 readFile()方法,可以异步的读取文件的内容,它的返回值是 Promise 的实例对象,因此可以调用.then()方法为每个 Promise 异步操作制定成功和失败之后的回调函数。
  1. import thenFs from 'then-fs'
  2. thenFs.readFile('./files/1.txt', 'utf8').then((r1) => console.log(r1))
  3. thenFs.readFile('./files/2.txt', 'utf8').then((r2) => console.log(r2))
  4. thenFs.readFile('./files/3.txt', 'utf8').then((r3) => console.log(r3))

运行结果:

222
333
111

可以看到无法保证文件的读取顺序

  1. import thenFs from 'then-fs'
  2. thenFs
  3. .readFile('./files/1.txt', 'utf-8')
  4. .then((r1) => {
  5. console.log(r1)
  6. return thenFs.readFile('./files/2.txt', 'utf8')
  7. })
  8. .then((r2) => {
  9. console.log(r2)
  10. return thenFs.readFile('./files/3.txt', 'utf8')
  11. })
  12. .then((r3) => {
  13. console.log(r3)
  14. })

运行结果:

111
222
333

可以通过.catch()捕获错误。

Promise.all()

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

  1. const promiseArr = [
  2. thenFs.readFile('./files/1.txt', 'utf8'),
  3. thenFs.readFile('./files/2.txt', 'utf8'),
  4. thenFs.readFile('./files/3.txt', 'utf8')
  5. ]
  6. Promise.all(promiseArr).then((result) => {
  7. console.log(result) //[ '111', '222', '333' ]
  8. })

Promise.race()

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

  1. const promiseArr = [
  2. thenFs.readFile('./files/1.txt', 'utf8'),
  3. thenFs.readFile('./files/2.txt', 'utf8'),
  4. thenFs.readFile('./files/3.txt', 'utf8')
  5. ]
  6. Promise.race(promiseArr).then((result) => {
  7. console.log(result) //谁快就把它当作result
  8. })

基于 Promise 封装读文件的方法

  1. 方法名称是getFile
  2. 方法要接受一个形参fpath,表示要读取的文件的路径
  3. 方法的返回值为Promise对象
  1. import fs from 'fs'
  2. function getFile(fpath) {
  3. return new Promise(function (resolve, reject) {
  4. fs.readFile(fpath, 'utf8', (err, data) => {
  5. if (err) return reject(err)
  6. resolve(data)
  7. })
  8. })
  9. }
  10. getFile('./files/1.txt')
  11. .then((success) => console.log(success))
  12. .catch((err) => console.log(err))

async/await

async/await是 ES8 引入的新语法,用来简化Promise操作,在async/await出现之前,开发者只能使用.then()

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

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

  1. //。async/await用法
  2. async function getAllFiles() {
  3. const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
  4. console.log(r1)
  5. const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
  6. console.log(r2)
  7. const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
  8. console.log(r3)
  9. }
  10. getAllFiles()

注意事项

  1. async/await 是成对出现的
  2. 在 async 方法中,第一个 await 之前的代码会同步执行,await 之后的代码会异步执行。
  1. console.log('A')
  2. async function getAllFiles() {
  3. console.log('B')
  4. const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
  5. const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
  6. const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
  7. console.log(r1, r2, r3)
  8. console.log('D')
  9. }
  10. getAllFiles()
  11. console.log('C')

A
B
C
111 222 333
D

EventLoop

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

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

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

同步任务和异步任务

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

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

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

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

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

  1. console.log('A')
  2. thenFs.readFile('./files/1.txt', 'utf8').then((dataStr) => {
  3. console.log('B')
  4. })
  5. setTimeout(() => {
  6. console.log('C')
  7. }, 0)
  8. console.log('D')

运行结果

A
D
C
B

A 和 D 属于同步任务,在主线程里面,会根据代码执行的先后顺序执行

B 和 C 属于异步任务,C 只是 0ms。相当于我把 C 委托给宿主环境,宿主环境不需要时间,就把回调函数放在了任务队列。然后 B 再执行

宏任务和微任务

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

  1. 宏任务
    • 异步 Ajax 请求
    • setTimeout,setInterval
    • 文件操作
    • 其他宏任务
  2. 微任务
    • Promise.then、Promise.catch 和 Promise.finally
    • Process.nextTick
    • 其他微任务

执行顺序:每一次宏任务执行完之后,都会检查是否存在待执行的微任务。

如果有,就会执行完所有微任务之后,再继续执行下一个宏任务

2 es6模块化 - 图1

宏任务和微任务理解

比如,去银行办业务。

  1. 小明和小红去银行办业务,需要取号排队
    • 宏任务队列
  2. 假设银行只有一个柜员,小明在办理业务的时候。小红只能等待
    • 单线程,宏任务按次序执行
  3. 小明办理完存款业务之后,柜员询问是否还想办理其他业务
    • 当前宏任务执行完,检查是否有微任务
  4. 小明会告诉柜员,还想办理理财产品和办张信用卡
    • 执行微任务,后续宏任务被推迟
  5. 小明离开柜台之后,柜员开始为小红办理
    • 所有微任务执行完毕,开始执行下一个叫宏任务

经典面试题

面试 1

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

运行结果

2
4
3
1

首先定时器是宏任务,只要 new 了一个 Promise 对象,就会执行这个函数。打印 2

然后。.then是微任务,所以执行同步任务打印 4

此时没有同步任务,就去微任务中,打印 3

最后执行宏任务打印 4

面试 2

  1. console.log('1')
  2. setTimeout(function () {
  3. console.log('2')
  4. new Promise(function (resolve) {
  5. console.log('3')
  6. resolve()
  7. }).then(function () {
  8. console.log('4')
  9. })
  10. })
  11. new Promise(function (resolve) {
  12. console.log('5')
  13. resolve()
  14. }).then(function () {
  15. console.log('6')
  16. })
  17. setTimeout(function () {
  18. console.log('7')
  19. new Promise(function (resolve) {
  20. console.log('8')
  21. resolve()
  22. }).then(function () {
  23. console.log('9')
  24. })
  25. })

运行结果

1
5
6
2
3
4
7
8
9