Node.js ES6模块语法&promise

216 阅读12分钟

ES6模块化

node.js 遵循了 CommonJS 的模块化规范。其中:

  • 导入其它模块使用 require() 方法
  • 模块对外共享成员使用module.exports 对象

ES6模块语法

ES6 的模块化主要包含如下 3 种用法:

  • 默认导出与默认导入
  • 按需导出与按需导入
  • 直接导入并执行模块中的代码

默认导出与默认导入


// 默认导出 `export default 默认导出的成员`
// export default a  // 导出一个值
export default {
  a,
  b,
  fn
}
// 默认导入时的接收名称可以任意名称,只要是合法的成员名称即可
//`import 接收名称 from '模块路径'`
import result from './xxx.js'
console.log(result)

注意点:

  • 每个模块中,只允许使用唯一的一次 export default
  • 默认导入时的接收名称可以任意名称,只要是合法的成员名称即可

按需导入与按需导出

//`export const s1 = 10`
export const a = 10
export const b = 20
export const fn = () => {
  console.log('内容')
}
//`import { 按需导入的名称 } from '模块标识符'`
import { a, b as c, fn } from './xxx.js'

注意事项:

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

直接导入模块(无导出)

如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。

此时,可以直接导入并执行模块代码,示例代码如下:

import '模块的路径'

//xxx.js
for (let i = 0; i < 10; i++) {
  console.log(i)
}


// 导入该模块
import './xxx.js'

Promise

Promise能够处理异步程序。

回调地狱

JS中或node中,都大量的使用了回调函数进行异步操作,而异步操作什么时候返回结果是不可控的,如果我们希望几个异步请求按照顺序来执行,那么就需要将这些异步操作嵌套起来,嵌套的层数特别多,就会形成回调地狱 或者叫做 横向金字塔

下面的案例就有回调地狱的意思:

案例:有 a.txt、b.txt、c.txt 三个文件,使用fs模板按照顺序来读取里面的内容,代码:

// 将读取的a、b、c里面的内容,按照顺序输出
const fs = require('fs');

// 读取a文件
fs.readFile('./a.txt', 'utf-8', (err, data) => {
    if (err) throw err;
    console.log(data.length);
    // 读取b文件
    fs.readFile('./b.txt', 'utf-8', (err, data) => {
        if (err) throw err;
        console.log(data);
        // 读取c文件
        fs.readFile('./c.txt', 'utf-8', (err, data) => {
            if (err) throw err;
            console.log(data);
        });
    });
});

案例中,只有三个文件,试想如果需要按照顺序读取的文件非常多,那么嵌套的代码将会多的可怕,这就是回调地狱的意思。

Promise简介

  • Promise对象可以解决回调地狱的问题
  • Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大
  • Promise可以理解为一个容器,里面可以编写异步程序的代码
  • 从语法上说,Promise 是一个对象,使用的使用需要 new

Promise简单使用

Promise是“承诺”的意思,实例中,它里面的异步操作就相当于一个承诺,而承诺就会有两种结果,要么完成了承诺的内容,要么失败。

所以,使用Promise,分为两大部分,首先是有一个承诺(异步操作),然后再兑现结果。

第一部分:定义“承诺”

// 实例化一个Promise,表示定义一个容器,需要给它传递一个函数作为参数,而该函数又有两个形参,通常用resolve和reject来表示。该函数里面可以写异步请求的代码
// 换个角度,也可以理解为定下了一个承诺
let p = new Promise((resolve, reject) => {
    // 形参resolve,单词意思是 完成
    // 形参reject ,单词意思是 失败
    fs.readFile('./a.txt', 'utf-8', (err, data) => {
        
        if (err) {
            // 失败,就告诉别人,承诺失败了
            reject(err);
        } else {
            // 成功,就告诉别人,承诺实现了
            resolve(data.length);
        }  
    });
});

第二部分:获取“承诺”的结果

// 通过调用 p 的then方法,可以获取到上述 “承诺” 的结果
// then方法有两个函数类型的参数,参数1表示承诺成功时调用的函数,参数2可选,表示承诺失败时执行的函数
p.then(
	(data) => {},
  (err) => {}
);

完整的代码:

const fs = require('fs');
// promise 承诺

// 使用Promise分为两大部分

// 1. 定义一个承诺
let p = new Promise((resolve, reject) => {
    // resolve -- 解决,完成了; 是一个函数
    // reject  -- 拒绝,失败了; 是一个函数
    // 异步操作的代码,它就是一个承诺
    fs.readFile('./a.txt', 'utf-8', (err, data) => {
        if (err) {
            reject(err);
        } else {
            resolve(data.length);
        }
    });
});

// 2. 兑现承诺
// p.then(
//     (data) => {}, // 函数类似的参数,用于获取承诺成功后的数据
//     (err) => {} // 函数类型的参数,用于或承诺失败后的错误信息
// );
p.then(
    (data) => {
        console.log(data);
    },
    (err) => {
        console.log(err);
    }
);

三种状态

  • 最初状态:pending,等待中,此时promise的结果为 undefined;
  • 当 resolve(value) 调用时,达到最终状态之一:fulfilled,(成功的)完成,此时可以获取结果value
  • 当 reject(error) 调用时,达到最终状态之一:rejected,失败,此时可以获取错误信息 error

当达到最终的 fulfilled 或 rejected 时,promise的状态就不会再改变了。

特点

当调用 resolve的时候,Promise 将到达最终的状态。 达到最终状态之后,Promise的状态就不会再改变了。

多次调用 resolve 函数,只有第一次有效,其他的调用都无效。

const fs = require('fs');

// 1. 创建 Promise 对象;(承诺)
let p = new Promise((resolve, reject) => {
  resolve(123);
  resolve(456); // 这次调用无效
});

// 2. 获取异步任务的结果
// p.then(函数1, [函数2]);
p.then(res => {
  console.log(res); // 123
}, err => {
  console.log(err);
});

同步异步?

  • new Promise是同步执行的
  • 获取结果时(调用 resolve 触发 then方法时)是异步的
console.log(1);

new Promise((resolve, reject) => {
  console.log(2);
  resolve();
  console.log(3);
}).then(res => {
  console.log(4); // 这里是异步的
})

console.log(5);

// 输出顺序: 1  2  3  5  4 ,因为只有 .then() 是异步的

then方法的链式调用

  • 前一个then里面返回的字符串,会被下一个then方法接收到。但是没有意义;

  • 前一个then里面返回的Promise对象,并且调用resolve的时候传递了数据,数据会被下一个then接收到

  • 前一个then里面如果没有调用resolve,则后续的then不会接收到任何值

    const fs = require('fs');
    // promise 承诺
    
    let p1 = new Promise((resolve, reject) => {
      fs.readFile('./a.txt', 'utf-8', (err, data) => {
        err ? reject(err) : resolve(data.length);
      });
    });
    
    let p2 = new Promise((resolve, reject) => {
      fs.readFile('./b.txt', 'utf-8', (err, data) => {
        err ? reject(err) : resolve(data.length);
      });
    });
    
    let p3 = new Promise((resolve, reject) => {
      fs.readFile('./c.txt', 'utf-8', (err, data) => {
        err ? reject(err) : resolve(data.length);
      });
    });
    
    p1.then(a => {
      console.log(a);
      return p2;
    }).then(b => {
      console.log(b);
      return p3;
    }).then(c => {
      console.log(c)
    }).catch((err) => {
      console.log(err);
    });
    

    catch 方法可以统一获取错误信息

封装按顺序异步读取文件的函数(了解)

function myReadFile(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, 'utf-8', (err, data) => {
            err ? reject(err) : resolve(data.length);
        })
    });
}


myReadFile('./a.txt')
.then(a => {
  console.log(a);
  return myReadFile('./b.txt');
})
.then(b => {
  console.log(b);
  return myReadFile('./c.txt');
})
.then(c => {
  console.log(c)
})
.catch((err) => {
  console.log(err);
});

使用第三方模块读取文件

  • npm init - y
  • npm i then-fs 安装then-fs模块
  • then-fs 将 内置的fs模块封装了,读取文件后,返回 Promise 对象,省去了我们自己封装
// then-fs 模块是第三方模块,需要 npm install then-fs  下载安装的
const fs = require('then-fs');

// then-fs 对内置的fs模块进行了重写的封装。调用方法后,返回Promise对象

let p1 = fs.readFile('./files/a.txt', 'utf-8');  // 
let p2 = fs.readFile('./files/b.txt', 'utf-8');
let p3 = fs.readFile('./files/c.txt', 'utf-8');

// 通过then获取结果
p1.then(res => {
  console.log(res.length);
  return p2;
}).then(res => {
  console.log(res.length);
  return p3;
}).then(res => {
  console.log(res.length);
})

async 和 await 修饰符

ES6 --- ES2015

async 和 await 是 ES2017 中提出来的。

异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题。

从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底。它们都有额外的复杂性,都需要理解抽象的底层运行机制。

异步I/O不就是读取一个文件吗,干嘛要搞得这么复杂?异步编程的最高境界,就是根本不用关心它是不是异步。

==async 函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案==。

ES2017 提供了async和await关键字。await和async关键词能够将异步请求的结果以返回值的方式返回给我们。

  • async 用于修饰一个 function
    • async 修饰的函数,总是返回一个 Promise 对象
    • 函数内的返回值,将自动包装在 resolved 的 promise 中
  • await 只能出现在 async 函数内
    • await 让 JS 引擎等待直到promise完成并返回结果
    • 语法:let value = await promise对象; // 要先等待promise对象执行完毕,才能得到结果
    • 由于await需要等待promise执行完毕,所以await会暂停函数的执行,但不会影响其他同步任务
  • 对于错误处理,可以选择在async函数后面使用 .catch() 或 在promise对象后使用 .catch()
const fs = require('fs');
// 将异步读取文件的代码封装
function myReadFile (path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, 'utf-8', (err, data) => {
            err ? reject(err) : resolve(data.length);
        });
    }).catch(err => {
        console.log(err);
    });
}

async function abc () {
    let a = await myReadFile('./a.txt');
    let b = await myReadFile('./b.txt');
    let c = await myReadFile('./c.txt');
    console.log(b);
    console.log(a);
    console.log(c);
}

abc();

错误处理

前提是得到几个Promise对象,代码如下:

let fs = require('then-fs');

let p1 = fs.readFile('./files/a.txt', 'utf-8');   
let p2 = fs.readFile('./files/bbb.txt', 'utf-8');  // 注意,这里故意写错路径
let p3 = fs.readFile('./files/c.txt', 'utf-8');

获取Promise的结果,可以通过 then 来获取。也可以通过 async 和 await 获取。

如果使用 then 获取结果,那么错误如何处理:在链式调用的尾端,加一个catch方法即可

// ----------------------  通过 then 获取结果       ----------------------------
p1.then(res => {
  console.log(res.length);
  return p2;
}).then(res => {
  console.log(res.length);
  return p3;
}).then(res => {
  console.log(res.length);
}).catch(err => {
  console.log(err);
})

如果使用async和await获取结果,最好的错误处理方案,就是使用 try ... catch ...

// ---------------------- 通过 async/await 获取结果 ----------------------------
async function abc() {
  try { // 尝试做一些事情
    let r1 = await p1; // 正常得到结果
    let r2 = await p2; // 这里出错了,就会抛出错误 throw err。
    let r3 = await p3;
    console.log(r1.length, r2.length, r3.length);
  } catch (e) {
    console.log(e); // catch这里,会抓住前面try里面抛出的错误
  }
}
abc();

小结

  • Promise是异步任务的一种解决方案
  • 得到Promise对象
    • 自己new Promise()
    • 自己封装函数,函数中返回 Promise对象
    • 使用第三方的模块或者库。比如 then-fs ,比如 axios
  • 获取结果
    • 通过 then 方法获取,并且通过 catch 捕获错误信息
    • 通过 async和await的配合获取结果,并且通过 try...catch...捕获错误

宏任务和微任务、事件循环

JavaScript是单线程的,也就是说,同一个时刻,JavaScript只能执行一个任务,其他任务只能等待。

为什么JavaScript是单线程的

js是运行于浏览器的脚本语言,因其经常涉及操作dom,如果是多线程的,也就意味着,同一个时刻,能够执行多个任务。

试想,如果一个线程修改dom,另一个线程删除dom,那么浏览器就不知道该先执行哪个操作。

所以js执行的时候会按照一个任务一个任务来执行。

为什么任务要分为同步任务和异步任务

试想一下,如果js的任务都是同步的,那么遇到定时器、网络请求等这类型需要延时执行的任务会发生什么?

页面可能会瘫痪,需要暂停下来等待这些需要很长时间才能执行完毕的代码

所以,又引入了异步任务。

  • 同步任务:同步任务不需要进行等待可立即看到执行结果,比如console
  • 异步任务:异步任务需要等待一定的时候才能看到结果,比如setTimeout、网络请求

事件循环(Event Loop)

事件循环的比较简单,它是一个在 "JavaScript 引擎等待任务","执行任务"和"进入休眠状态等待更多任务"这几个状态之间转换的无限循环。

引擎的一般算法:

  1. 当有任务时:
    • 从最先进入的任务开始执行。
  2. 没有其他任务,休眠直到出现任务,然后转到第 1 步。

任务队列

根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise 等API便是任务源。

在事件循环中,每进行一次循环的关键步骤如下:

  • 在此次循环中选择最先进入队列的任务(oldest task),如果有则执行(一次)
  • 检查是否存在 微任务(Microtasks),如果存在则不停地执行,直至清空 微任务队列(Microtasks Queue)
  • 更新 render(DOM渲染)
  • 以上为一次循环,主线程重复执行上述步骤

在上述循环的基础上需要了解几点:

  • JS分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,宿主环境管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

图片描述

宏任务

(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

任务(代码)宏任务环境
script宏任务浏览器
事件宏任务浏览器
网络请求(Ajax)宏任务浏览器
setTimeout() 定时器宏任务浏览器 / Node
fs.readFile() 读取文件宏任务Node

比如去银行排队办业务,每个人的业务就相当于是一个宏任务;

微任务

微任务(microtask)是宏任务中的一个部分,它的执行时机是在同步代码执行之后,下一个宏任务执行之前。

微任务包含:
Promise.then
process.nextTick(Node.js 环境)

比如一个人,去银行存钱,存钱之后,又进行了一些了操作,比如买纪念币、买理财产品、办信用卡,这些就叫做微任务。

运行机制

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(执行栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务里的同步代码执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)