目录
2.3.1 基于回调函数按顺序读取文件内容(存在回调地狱的问题)
2.5 Promise.all()和Promise.race()
一、ES6模块化
1.1 node.js的模块化
node.js遵循了CommonJS的模块化规范,其中:导入其它模块使用require()方法。模块对外共享成员使用module.exports对象
模块化的好处:大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大地方便了各个模块之间的互相调用。
1.2 前端模块化规范的分类
在ES6模块化规范诞生之前,JavaScript社区已经尝试并提出AMD、CMD、CommonJS等模块化规范。但是,这些由社区提出的模块化规范,还是存在一定的差异性和局限性,并不是浏览器与服务器通用的模块化规范。如
- AMD和CMD适用于浏览器端的JavaScript模块化
- CommonJS适用于服务器端的JavaScript模块化
太多的模块化给开发者增加了开发成本和难度,因此,大一统的ES6模块化诞生了!
1.3 ES6模块化
ES6模块化是浏览器和服务器端通用的模块化开发规范,开发者不需额外学习AMD、CMD、CommonJS等规范。
ES6模块化规范中定义:
- 每个js文件都是一个独立的模块
- 导入其它模块成员使用export关键字
- 向外共享模块成员使用export关键字
1.4 在node.js中体验ES6模块化
node.js中默认仅支持CommonJS模块化规范,若想基于node.js学习ES6的模块化语法,可按照如下两个步骤进行配置:
(1)确保安装了v14.15.1或更高版本的node.js。cmd窗口输入node -v查看
(2)npm init -y新建package.json文件,并在根节点中添加"type":"module"节点
1.5 ES6模块化的基本语法
ES6模块化主要包括以下3种用法:
(1)默认导出与默认导入
(2)按需导出与按需导入
(3)直接导入并执行模块中的代码
1.5.1 默认导出与默认导入
默认导出的语法:export default 默认导出的成员。
在项目下新建export.js文件,代码如下:
// 定义模块私有成员n1,n2
let n1 = 10
let n2 = 20
// 定义模块私有方法show
function show() {}
// 使用export default默认导出语法,向外共享n1和show两个成员
export default {
n1,
show
}
默认导入的语法:import 接收名称 from '模块标识符'
在在项目下新建import.js文件,代码如下:
import m1 from './export.js'
console.log(m1)
node import.js执行js文件,打印内容为:{ n1: 10, show: [Function: show] }
注意事项:每个模块中,只允许使用唯一一次export default,否则会报错
默认导入时的接收名称可以是任意名称,只要是合法的名称即可。如数字开头的为不合法
1.5.2 按需导出与按需导入
按需导出:export 按需导出的成员
按需导入:import {s1} from '模块标识符'
按需导出和按需导入的注意事项:
(1)每个模块中可以使用多次按需导出
(2)按需导入的成员名称必须和按需导出的名称保持一致
(3)按需导入时,可以使用as关键字进行重命名
(4)按需导入可以和默认导出一起使用
1.5.3 直接导入并执行模块中的代码
如果只想单独地执行某个模块中的代码,并不需要得到模块中向外共享的成员,可以直接导入并执行模块化代码:
// 01.js文件
for (let i=0;i<3;i++) {
console.log(i)
}
// 02.js文件
import './01.js'
执行node 02.js,输出如下:
二、Promise
2.1 回调地狱
多层回调函数的相互嵌套,就形成了回调地狱,示例代码如下:
缺点:
- 代码耦合性太强,牵一发而动全身,难以维护
- 大量冗余的代码相互嵌套,代码的可读性差
ES6提供Promise解决回调地狱的问题
2.2 Promise的基本概念
(1)Promise是一个构造函数
- 可创建Promise的实例,const p=new Promise()
- new出来的实例对象,代表一个异步操作
(2)Promise.protype上包含一个.then方法
- 每一次new Promise()构造函数得到的实例对象,都可以通过原型链的方式访问到.then方法,例如p.then()
(3).then()方法用来预先指定成功和失败的回调函数
- p.then(成功的回调函数,失败的回调函数)
- p.then(result => {}, error => {})
- 调用.then方法时,成功的回调函数是必选的,失败的回调函数是可选的
2.3 基于Promise读取文件内容
2.3.1 基于回调函数按顺序读取文件内容(存在回调地狱的问题)
2.3.2 基于then-fs读取文件内容
由于node.js官方提供的fs模块仅支持以回调函数的方式读取文件,不支持Promise的调用方式,因此,要先安装then-fs这个第三方包,从而支持我们基于Promise的方式读取文件内容:
(1)then-fs包安装:npm install then-fs
(2)调用then-fs的readFile(),可以异步地读取文件内容。返回值是Promise的实例对象。因此,可以调用.then()方法为每个Promise异步操作指定成功和失败之后的回调函数。示例代码如下【存在问题】:
import thenFs from "then-fs";
thenFs.readFile('./files/1.txt', 'utf8').then(r1 => {console.log(r1)}, err1 => {console.log(err1.message)})
thenFs.readFile('./files/2.txt', 'utf8').then(r2 => {console.log(r2)}, err2 => {console.log(err2.message)})
thenFs.readFile('./files/3.txt', 'utf8').then(r3 => {console.log(r3)}, err3 => {console.log(err3.message)})
存在问题:不能保证文件读取的顺序。需要进一步改进
.then()方法的特性:如果上一个.then()方法中返回了一个新的Promise实例对象,则可以通过.then继续进行处理。Promise支持链式调用,解决回调地狱的问题,如下:
import thenFs from "then-fs";
thenFs.readFile('./files/1.txt', 'utf8')
.then((r1) => {
console.log(r1)
// .then中返回一个Promise对象
return thenFs.readFile('./files/2.txt', 'utf8')
})
.then((r2) => {
console.log(r2)
return thenFs.readFile('./files/3.txt', 'utf8')
})
.then((r3) => {
console.log(r3)
})
2.4 通过.catch捕获错误
在Promise的链式操作中如果发生了错误,可以使用Promise.protype.catch方法进行捕获和处理:
import thenFs from "then-fs";
thenFs.readFile('./files/1.txt', 'utf8')
.then((r1) => {
console.log(r1)
// .then中返回一个Promise对象
return thenFs.readFile('./files/2.txt', 'utf8')
})
.then((r2) => {
console.log(r2)
return thenFs.readFile('./files/3.txt', 'utf8')
})
.then((r3) => {
console.log(r3)
})
.catch(err => {
console.log(err.message)
})
如果不希望前面的错误导致后续的.then无法正常执行,则可以将.catch的调用提前,如下:
import thenFs from "then-fs";
thenFs.readFile('./files/1.txt', 'utf8')
.catch(err => {
console.log(err.message)
})
.then((r1) => {
console.log(r1)
// .then中返回一个Promise对象
return thenFs.readFile('./files/2.txt', 'utf8')
})
.then((r2) => {
console.log(r2)
return thenFs.readFile('./files/3.txt', 'utf8')
})
.then((r3) => {
console.log(r3)
})
2.5 Promise.all()和Promise.race()
1.txt文件内容1,2.txt文件内容2,3.txt文件内容3
Promise.all()方法会发起并行的Promise异步操作,等所有的异步操作全部结束后才会执行下一步操作,如下:
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(promiseArr).then(result => {
console.log(result)
})
Promise.race()方法会发起并行的Promise异步操作,只要任何一个异步操作完成,就立即执行下一步的.then操作(赛跑机制),示例如下:每次都不一样
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(promiseArr).then(result => {
console.log(result)
})
2.6 基于Promise封装读文件的方法
方法的封装要求:
- 方法的名称要定义为getFile
- 方法接收一个形参fPath,表示要读取的文件的路径
- 方法的返回值为一个Promise实例对象
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/1.txt').then(r1 => {console.log(r1)}, err => {console.log(err.message)})
三、async、await
3.1 什么是async、await
async、await是ES6引入的新语法,用来简化Promise异步操作。在async、await出现之前,开发者只能通过链式.then()的方式处理Promise异步操作,示例代码如下:
import thenFs from "then-fs";
thenFs.readFile('./files/1.txt', 'utf8')
.catch(err => {
console.log(err.message)
})
.then((r1) => {
console.log(r1)
// .then中返回一个Promise对象
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链式调用解决了回调地狱的问题,但是代码冗余、阅读性差、不易理解
使用async、await来解决该问题!!!(见3.2)
3.2 async、await的基本使用
使用async、await简化Promise异步操作的示例代码如下:
import thenFs from "then-fs";
// 按照顺序读取文件1、2、3的内容
async function getAllFiles() {
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)
}
// 调用
getAllFiles()
3.2 async、await的使用注意事项
(1)如果在function中使用了await,则function必须被async修饰
(2)在async方法中,第一个await之前的代码会同步执行,await之后的代码会异步执行
import thenFs from "then-fs";
// 按照顺序读取文件1、2、3的内容
console.log('A')
async function getAllFiles() {
console.log('B')
const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
console.log(r1)
console.log(r2)
console.log(r3)
console.log('C')
}
// 调用
getAllFiles()
console.log('D')
如上,代码的执行结果为:
四、EventLoop
4.1 JavaScipt是单线程的语言
JavaScript是一门单线程的编程语言,也就说,同一时间只能做一件事情。
单线程政治性任务队列的问题:如果前一个任务非常耗时,则后续的任务就不得不一直等待,从而导致程序假死的问题。
4.2 同步任务和异步任务
为了防止某个耗时任务导致程序假死的问题,JavaScript把执行的任务分成了两类:
(1)同步任务(synchronous)
- 又叫做非耗时任务,指的是在主线程上排队执行的那些任务。
- 只有前一个任务执行完毕,才能执行后一个任务
(2)异步任务(asynchronous)
- 又叫做耗时任务,异步任务由JavaScript委托给寄主环境进行执行
- 当异步任务执行完成后,会通知JavaScript主线程执行异步任务的回调函数
4.3 同步任务和异步任务的执行过程
(1)同步任务由JavaScript主线程顺序执行、异步任务委托给宿主环境执行
(2)已完成的异步任务对应的回调函数,会被加入到任务队列中等待执行。
(3)JavaScript主线程的执行栈被清空后,会读取任务队列中的回调函数,顺序执行。
(4)JavaScript主线程不断重读上面的三步
4.4 EventLoop的基本概念
JavaScript主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行,这个过程是循环不断的,这个运行机制称为EventLoop(事件循环)
分析如上代码的执行结果为:ADCB
AD属于同步任务,C延时任务为0毫秒,所以会先被执行
五、宏任务和微任务
5.1 什么是宏任务和微任务
JavaScript把异步任务又做了进一步的划分,异步任务又分为两类,分别是:
(1)宏任务
- 异步Ajax请求
- setTimeOut、setInteval
- 文件操作
- 其它宏任务
(2)微任务
- Promise.then、.catch和finally
- process.nextTick
- 其它微任务
5.2 宏任务和微任务的执行顺序
每一个宏任务执行完之后,都会检查是否存在待执行的微任务。如果有,则执行完所有的微任务后,再继续执行下一个宏任。
六、API接口案例
6.1 案例需求
基于MYSQL数据库+Express对外提供用户列表的API接口服务。用到的技术点如下:
- 第三方包express和mysql2
- ES6模块化
- Promise
- async/await
6.2 实现步骤
- 搭建项目的基本结构
- 搭建基本的服务器
- 创建db数据库操作模块
- 创建user_ctrl业务模块
- 创建user_route路由模块
6.3 搭建项目的基本结构
- 启用ES6模块化支持:npm init -y生成package.json文件,并在文件中声明"type":"module"
- 安装第三方依赖包:npm install express@4.17.1 mysql2@2.2.5
6.4 搭建基本的服务器
根目录下新建app.js的文件,代码如下:
import express from "express";
const app = express()
app.listen(80, () => {
console.log('server start....')
})
6.5 创建db数据库操作模块
根目录下新建db文件夹,并在db文件夹下新建index.js文件,代码如下:
import mysql from 'mysql2'
const pool = mysql.createPool({
host: '127.0.0.1',
port: 3306,
database: 'my_db_01',
user: 'root',
password: 'admin123'
})
// 默认导出一个支持Promise API的pool
export default pool.promise()
6.6 创建user_ctrl业务模块
根目录下新建controller目录,并在其下新建user_ctrl.js文件,代码如下:
import db from '../db/index.js'
// 获取所有用户的列表数据
// ES6的按需导出,将getAllUser方法导出
export async function getAllUser(req, res) {
// db.query函数的返回值是Promise的实例对象,使用await/async简化
const [rows] = await db.query('select id,gender from user')
res.send({
status: 0,
message: '获取用户表数据成功',
data: rows,
})
}
6.7 创建user_route路由模块
根目录下新建router文件夹,并新建user_router.js文件,代码如下:
import express from 'express'
// 从user_ctrl.js中按需导入getAllUser函数
import { getAllUser } from '../controller/user_ctrl.js'
// 创建路由对象
const router = new express.Router()
// 挂载路由规则
router.get('/user', getAllUser)
// 使用ES6的默认导出语法,将路由对象共享出去
export default router
将路由模块挂载。修改6.4中创建的app.js文件:
import express from "express";
// 1、导入路由对象
import UserRouter from './router/user_router.js'
const app = express()
// 2、挂载路由模块
app.use('/api', UserRouter)
app.listen(80, () => {
console.log('server start....')
})
postman中测试:输入http://127.0.0.1/api/user
6.8 使用try...catch捕获异常
import db from '../db/index.js'
// 获取所有用户的列表数据
// ES6的按需导出,将getAllUser方法导出
export async function getAllUser(req, res) {
// db.query函数的返回值是Promise的实例对象,使用await/async简化
try {
const [rows] = await db.query('select id,gender,xxx from user')
res.send({
status: 0,
message: '获取用户表数据成功',
data: rows,
})
}catch(e) {
res.send({
status: 1,
message: '获取用户表数据失败!',
desc: e.message,
})
}
}
// 调用
getAllUser()