- 能够知道如何使用ES6的模块化语法
- 能够知道如何使用Promise解决回调地狱的问题
- 能够知道如何使用async/await简化Promise的调用
- 能够说出什么是EventLoop
- 能够说出宏任务和微任务的执行顺序
node.js
中如何实现模块化node.js
遵循CommonJS
的模块化规范。其中
require()
方法使用module.exports
对象模块化的好处:
大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用,利人利己。
ES6模块化规范是浏览器端和服务器端通用的模块化开发规范。它的出现极大降低了前端开发者模块化学习成本。
ES6模块化规范中定义:
import
关键字export
关键字node.js 中默认支持CommonJS模块化规范
确保如下配置
1. 确保安装了v14.15.1 或更高版本的node.js
使用如下命令初始化包管理工具
npm init -y
"type":"module "
节点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 }
多层回调函数的相互嵌套,就形成了回调地狱
setTimeout(() => {
console.log("延迟 1 秒后输出");
setTimeout(() => {
console.log("延迟 2 秒后输出");
setTimeout(() => {
console.log("延迟 3 秒后输出");
}, 3000)
}, 2000)
}, 1000)
回调地狱的缺点:
为了解决回调地狱的问题,ES6中新增了Promise的概念
Promise 是一个构造函数
Promise.prototype 上包含了一个.then() 方法
.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 成功
})
})
})
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);})
上述代码,无法保证文件的读取顺序,需要进一步的改进
如果上一个.then()方法返回了一个新的Promise实例对象,则可以通过下一个.then()继续进行处理,通过.then()方法的链式调用,就解决了回调地狱的问题
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)
})
在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异步操作,等所有的异步操作全部结束以后才会执行下一步的.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异步操作,只要任何一个异步操作完成,就立即执行下一步的.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); // 谁执行的快,就输出谁的结果
})
方法的封装要求
getFile
fpath
,表示要读取的文件的路径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() 指定的成功和失败的回调函数,可以在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 是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
JavaScript 是单线程的语言
同步任务和异步任务
同步任务和异步任务的执行过程
同步任务由JavaScript主线程依次执行
异步任务委托给宿主环境(浏览器或者node.js)执行
已完成的异步任务对应的回调函数,会被加入到任务队列中等待执行
JavaScript主线程的执行栈被清空后,会读取任务序列中的回调函数,次序执行
JavaScript主线程不断重复上面的第4步
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 属于同步任务,它们的回调函数会被加入到任务序列中,等待主线程空闲时再执行
什么是宏任务和微任务
JavaScript 把异步任务又进一步的划分,异步任务分为两类
1. 宏任务
- 异步Ajax请求
- setTimeout 、 setInterval
- 文件操作
- 其他宏任务
2. 微任务
- Promise.then .catch .finally
- process.nextTick
- 其他微任务
每一个宏任务执行完毕之后,都会检查是否存在待执行的微任务
如果有,则执行所有微任务之后,再继续执行下一个宏任务
举例分析宏任务与微任务的执行过程
经典面试题:
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
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
基于MySQL + Express 对外提供用户列表的API 接口服务
使用到的技术点
第三方包 express 和 MySQL 2
ES6 模块化
Promise
async/await
主要的实现步骤
搭建项目的基本结构
npm init -y
"type": "module"
npm install [email protected] [email protected]
创建基本的服务器 app.js
import express from 'express'
const app = express()
app.listen(80,()=>{
console.log("服务器已启动: http://127.0.0.1");
})
运行 node app.js
创建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: '1234'
})
export default pool.promise()
创建user_ctrl 模块
数据库建表
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);
根目录创建controller/user_ctrl.js
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
})
}
创建user_router模块 router/user_router.js
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
app.js
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");
})
在ApiPost 中测试
使用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
})
}
}