ES6 模块化

ES6 模块化

学习目标

- 能够知道如何使用ES6的模块化语法
- 能够知道如何使用Promise解决回调地狱的问题
- 能够知道如何使用async/await简化Promise的调用
- 能够说出什么是EventLoop
- 能够说出宏任务和微任务的执行顺序

ES6模块化

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

  • node.js 遵循CommonJS的模块化规范。其中
    • 导入其他模块使用require() 方法
    • 模块对外共享成员使用module.exports 对象

模块化的好处:

​ 大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用,利人利己。

2.什么是ES6模块化规范

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

ES6模块化规范中定义:

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

3.在node.js 中体验ES6的模块化

node.js 中默认支持CommonJS模块化规范

​ 确保如下配置

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

​ 使用如下命令初始化包管理工具

npm init -y
  1. 在package.json 的根节点中添加 "type":"module "节点

4.ES6模块化的基本语法

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

4.1

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

let n1 = 10;  // 定义模块私有成员 n1
let n2 = 20;  // 定义模块私有成员 n2 (外界访问不到n2 ,因为它没有被共享出去)

function show() {}  // 定义模块私有方法  show

export default {  // 使用export default 默认导出语法,向外共享 n1 和 show 两个成员
  n1,
  show
}

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

import m1 from "./01_m1.js"

console.log(m1)
// { n1: 10, show: [Function: show] }

​ 默认导出的注意事项

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

​ 默认导入的注意事项

​ 默认导入时的接收名称可以是任意名称,只要是合法的成员名称即可 例:不能以数字开头

4.2

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

// 向外按需导出变量s1
export let s1 = 'aaa'
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. 每个模块中可以使用**多次**按需导出
1. 按需**导入的成员名称**必须和按需**导出的名称**保持一致
1. 按需导入时,可以使用==as关键字==进行重命名
1. 按需导入可以和默认导入一起使用
// 向外按需导出变量s1
export let s1 = 'aaa'
export let s2 = 'ccc'

// 向外按需导出方法 say
export function say() {}
// 默认导出
export default {  
  a: 20
}
import info, {  // info 用来接收默认导出
  s1,
  s2,
  say
} from './03_m2.js'

console.log(s1); // aaa
console.log(s2); // ccc
console.log(say); //  [Function: say]

console.log(info) // { a: 20 }

Promise

回调地狱

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

setTimeout(() => {
  console.log("延迟 1 秒后输出");
  setTimeout(() => {
    console.log("延迟 2 秒后输出");
    setTimeout(() => {
      console.log("延迟 3 秒后输出");
    }, 3000)
  }, 2000)
}, 1000)

回调地狱的缺点:

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

为了解决回调地狱的问题,ES6中新增了Promise的概念

Promise 基本概念

  1. ​ Promise 是一个构造函数

    1. 可以创建Promise 的实例 const p = new Promise()
    2. new 出来的Promise实例对象,代表一个异步操作
  2. Promise.prototype 上包含了一个.then() 方法

    • 每一次new Promise() 构造函数得到的实例对象都可以通过原型链的方式访问到.then() 方法,例如 p.then()
  3. .then() 方法用来预先指定成功和失败的回调函数

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

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

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

基于then-fs读取文件内容

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

​ 安装then-fs 第三方包,基于Promise的方式读取文件内容

npm install then-fs

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

import thenFs from 'then-fs'
thenFs.readFile('./1.txt','utf8').then(r1 =>{console.log(r1)},err1=>{console.log(err1.message);})
thenFs.readFile('./2.txt','utf8').then(r2 =>{console.log(r2)},err2=>{console.log(err2.message);})
thenFs.readFile('./3.txt','utf8').then(r3 =>{console.log(r3)},err3=>{console.log(err3.message);})

ES6 模块化_第1张图片

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

then() 方法的特性

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

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

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

import thenFs from 'then-fs'
thenFs.readFile('./1.txt', 'utf8')  // 返回值 是 Promise 的实例对象
  .then(r1 => {         // 通过.then 为第一个 Promise 实例指定成功之后的回调函数
    console.log(r1)
    return thenFs.readFile('./2.txt', 'utf8')  // 在第一个 .then 中返回一个新的Promise 实例对象
  })
  .then(r2 => {
    console.log(r2)
    return thenFs.readFile('./3.txt', 'utf8')
  })
  .then(r3 => {
    console.log(r3)
  })

通过.catch() 捕获错误

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

​ 将.catch 放到.then 之前,则 之后的.then 没有错误时可以正常执行

import thenFs from 'then-fs'
thenFs.readFile('./11.txt', 'utf8') 
  .catch(err => { // 捕获 发生的错误 输出错误信息
    console.log(err.message) // ENOENT: no such file or directory, open 'F:\WEB\ES6\demo1\11.txt'
  })
  .then(r1 => {
    return thenFs.readFile('./2.txt', 'utf8')
  })
  .then(r2 => {
    console.log(r2)
    return thenFs.readFile('./3.txt', 'utf8')
  })
  .then(r3 => {
    console.log(r3)
  })

Promise.all()

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

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

// 1.定义一个数组,存放3个读文件的异步操作
import thenFs from 'then-fs'
const promiseArr = [
  thenFs.readFile('./1.txt', 'utf8'),
  thenFs.readFile('./2.txt', 'utf8'),
  thenFs.readFile('./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);
  })
// 111 222 333

Promise.race()

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

import thenFs from 'then-fs'
const promiseArr = [
  thenFs.readFile('./1.txt', 'utf8'),
  thenFs.readFile('./2.txt', 'utf8'),
  thenFs.readFile('./3.txt', 'utf8'),
]

Promise.race(promiseArr)
  .then(result => {
    console.log(result); // 谁执行的快,就输出谁的结果
  })

基于Promise封装读文件的方法

方法的封装要求

  1. ​ 方法名称要定义为getFile
  2. ​ 方法接收一个形参fpath,表示要读取的文件的路径
  3. ​ 方法的返回值为Promise实例对象
function getFile(fpath){
  return new Promise()  // 形式上的异步操作  不知道是读文件的还是发ajax的
}

创建具体的异步操作

​ 需要在new Promise() 构造函数期间,传递一个funciton函数,将具体的异步操作定义到function函数内部

function getFile(fpath) {
  return new Promise(function () {
    // 这是一个具体的、读文件的异步操作
    FileSystem.readFile(fpath, 'utf8', (err, dataStr) => {})
  })
}

获取.then 的两个实参

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

import fs from 'fs'

function getFile(fpath) {
  // resolve 是成功的回调函数    reject 是 失败的回调函数
  return new Promise(function (resolve, reject) {
    fs.readFile(fpath, 'utf8', (err, dataStr) => {
      if (err) return reject(err)
      resolve(dataStr)
    })
  })
}

// getFile('./1.txr').then(成功的回调函数, 失败的回调函数)
getFile('./1.txt').then((r1) => {
  console.log(r1)
}, (err) => {
  console.log(err);
})
getFile('./11.txt').then((r1) => {
  console.log(r1)
}).catch(err => console.log(err.message))
//ENOENT: no such file or directory, open 'F:\WEB\ES6\demo1\11.txt' 

async / await

async / await 是ES8 引入的新语法,用来简化 Promise 异步操作。

在async/await 出现之前,开发者只能通过链式.then()的方式处理Promise异步操作

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

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

基本使用

import thenFs from 'then-fs'

async function getAllFiles() {
  const r1 = await thenFs.readFile('./1.txt', 'utf8')
  console.log(r1);
  const r2 = await thenFs.readFile('./2.txt', 'utf8')
  console.log(r2);
  const r3 = await thenFs.readFile('./3.txt', 'utf8')
  console.log(r3);
}

getAllFiles()

不加await 返回值就是一个Promise实例,加上之后就是读取到文件的内容

注意事项

1. 如果在function中使用了await,则funciton必须被async修饰
1. 在async方法中,==第一个await之前的代码会同步执行==,await之后的代码会异步执行
import thenFs from 'then-fs'

console.log('A');
async function getAllFiles() {
  console.log('B');
  const r1 = await thenFs.readFile('./1.txt', 'utf8')
  const r2 = await thenFs.readFile('./2.txt', 'utf8')
  const r3 = await thenFs.readFile('./3.txt', 'utf8')
  console.log(r1, r2, r3);
  console.log('D');

}
getAllFiles()
console.log('C');
// A
// B
// C
// 111 222 333
// D

EventLoop

  1. JavaScript 是单线程的语言

    • 单线程执行任务序列的问题:
      • 如果前一个任务非常耗时,则后续任务就不得不一致等待,从而导致程序假死的问题
  2. 同步任务和异步任务

    • 为了防止某个耗时任务导致程序假死的问题,JavaScript把待执行的任务分成了两类:
      1. 同步任务
        • 又叫做非耗时任务,指的是在主线程上排队执行的那些任务
        • 只有前一个任务执行完毕,才能执行后一个任务
      2. 异步任务
        • 又叫做耗时任务,异步任务由JavaScript委托给宿主环境进行执行
        • 当异步任务执行完成后,会通知JavaScript主线程执行异步任务的回调函数
  3. 同步任务和异步任务的执行过程

  4. 同步任务由JavaScript主线程依次执行

  5. 异步任务委托给宿主环境(浏览器或者node.js)执行

  6. 已完成的异步任务对应的回调函数,会被加入到任务队列中等待执行

  7. JavaScript主线程的执行栈被清空后,会读取任务序列中的回调函数,次序执行

  8. JavaScript主线程不断重复上面的第4步

  9. EventLoop 的基本概念

    JavaScript主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中一次执行

    这个过程是循环不断的,所以整个的这种运行机制又被称为EventLoop(事件循环)

  import thenFs from 'then-fs'
  
  console.log('A');
  thenFs.readFile('./1.txt', 'utf8').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张图片

每一个宏任务执行完毕之后,都会检查是否存在待执行的微任务

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

举例分析宏任务与微任务的执行过程

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

经典面试题:

setTimeout(function () {
  console.log('1');
})

new Promise(function (resolve) {
  console.log('2');
  resolve()
}).then(function(){
  console.log('3');
})


console.log('4');

2 4 3 1

  1. 先执行所有的同步任务
  2. 再执行微任务
console.log('1');
setTimeout(function(){
  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(){
  console.log('7');
  new Promise(function(resolve){
    console.log('8');
    resolve()
  }).then(function(){
    console.log('9');
  })
})


1 5 6 2 3 4 7 8 9

1 5 是同步任务 微任务 6

宏任务队列中 2 3 执行微任务 4

下一个宏任务 7 8 执行微任务 9

API接口案例

基于MySQL + Express 对外提供用户列表的API 接口服务

  1. 使用到的技术点

    • 第三方包 express 和 MySQL 2

    • ES6 模块化

    • Promise

    • async/await

  2. 主要的实现步骤

    1. 搭建项目的基本结构
    2. 创建基本的服务器
    3. 创建db数据库操作模块
    4. 创建user_ctrl业务模块
    5. 创建user_router路由模块
  3. 搭建项目的基本结构

    1. 启用ES6模块化支持
      1. npm init -y
      2. 在package.json 中 声明 "type": "module"
    2. 安装第三方依赖包
  4. 创建基本的服务器 app.js

    import express from 'express'
    const app = express()
    
    app.listen(80,()=>{
      console.log("服务器已启动: http://127.0.0.1");
    })
    

    ​ 运行 node app.js

  5. 创建db数据块操作模块

    1. 在根目录创建db文件夹,在db文件夹下创建index.js

    2. import mysql from 'mysql2'
      
      const pool = mysql.createPool({
        host: '127.0.0.1',
        port: 3306,
        database: 'my_db_01',
        user: 'root',
        password: '1234'
      })
      
      export default pool.promise()
      
  6. 创建user_ctrl 模块

    1. 数据库建表

      CREATE TABLE t_student(
      	sid int PRIMARY key auto_increment,
      	sname varchar(50),
      	sage int
      );
      
      insert into t_student(sname,sage) values('Lucy',25);
      insert into t_student(sname,sage) values('Lili',20);
      insert into t_student(sname,sage) values('Jim',20);
      
    2. 根目录创建controller/user_ctrl.js

    3. import db from '../db/index.js'
      
      // 使用ES6 的按需导出语法,将getAllUser方法导出出去
      // db.query() 返回值默认是一个promise对象,所以使用async/await 接收 并用 [rows] 解构
      export async function getAllUser(req, res) {
        const [rows] = await db.query('select sid,sname,sage from t_student')
        res.send({
          status:0,
          message:'获取用户列表成功',
          data:rows
        })
      }
      
      
  7. 创建user_router模块 router/user_router.js

    1. import express from 'express';
      import {
        getAllUser
      } from '../controller/user_ctrl.js'
      // 创建路由对象
      const router = new express.Router()
      // 挂载路由规则  用户访问/user 时,将此次请求传给getAllUser
      router.get('/user', getAllUser)
      
      export default router
      
    2. app.js

      1. import express from 'express'
        import userRouter from './router/user_router.js'
        const app = express()
        // 挂载路由,如果用户是以/api  访问,则指定userRouter路由模块
        app.use('/api',userRouter)
        
        app.listen(80,()=>{
          console.log("服务器已启动: http://127.0.0.1");
        })
        
      2. 在ApiPost 中测试

ES6 模块化_第3张图片

  1. 使用try…catch 捕获异常

    user_ctrl.js

    import db from '../db/index.js'
    
    // 使用ES6 的按需导出语法,将getAllUser方法导出出去
    export async function getAllUser(req, res) {
      try {  // 捕获异常
        const [rows] = await db.query('select sid,sname,sage from t_student')
        res.send({
          status: 0,
          message: '获取用户列表成功',
          data: rows
        })
      } catch (e) {
        res.send({
          status: 1,
          message: '获取用户列表失败',
          desc: e.message
        })
      }
    }
    

你可能感兴趣的:(node.js,其他,node.js)