03-ES6模块化和Promise(必学) - 图1

ES6模块化

模块化的分类

在 ES6 模块化规范诞生之前,JavaScript 社区已经尝试并提出了AMDCMDCommonJS 等模块化规范。
但是,这些由社区提出的模块化标准,还是存在一定的差异性与局限性、并不是浏览器与服务器通用的模块化
标准,例如:

  • AMDCMD 适用于浏览器端的 Javascript 模块化
  • CommonJS 适用于服务器端的 Javascript 模块化
    • 导出:module.exports = 导出的内容
    • 导入:require(‘模块’)

太多的模块化规范给开发者增加了学习的难度与开发的成本。因此,官方的ES6 模块化规范诞生了!

为什么要学习ES6 模块化规范

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

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

    在nodejs中使用ES6模块化

    node.js 中默认仅支持 CommonJS 模块化规范,若想基于 node.js 体验与学习 ES6 的模块化语法,可以按照
    如下两个步骤进行配置:
  1. 确保安装了 v13.0.0 或更高版本的 node.js
  2. 在 package.json 的根节点中添加 "type": "module" 节点
  • type,默认是commonjs,表示项目代码 只能 使用 CommonJS 语法(只能使用 module.exports 导出,使用require导入)
  • type 配置为 “module” 之后,表示项目代码 只能 使用 ES6模块化语法

    ES6模块语法

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

  • 默认导出与默认导入

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

    默认导出与默认导入

    默认导出的语法: export default 默认导出的成员
    默认导入的语法: import 接收名称 from '模块路径'

  • 导出

    1. const a = 10
    2. const b = 20
    3. const fn = () => {
    4. console.log('这是一个函数')
    5. }
    6. // 默认导出
    7. // export default a // 导出一个值
    8. export default {
    9. a,
    10. b,
    11. fn
    12. }
  • 导入

    1. // 默认导入时的接收名称可以任意名称,只要是合法的成员名称即可
    2. import result from './xxx.js'
    3. console.log(result)

    注意点:

  • 每个模块中,只允许使用唯一的一次 export default

  • 默认导入时的接收名称可以任意名称,只要是合法的成员名称即可

    按需导入与按需导出

    按需导出的语法: export const s1 = 10
    按需导入的语法: import { 按需导入的名称 } from '模块标识符'

    1. export const a = 10
    2. export const b = 20
    3. export const fn = () => {
    4. console.log('内容')
    5. }

    按需导入的语法

    1. import { a, b as c, fn } from './xxx.js'

    注意事项:

  • 每个模块中可以有多次按需导出

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

    将所有内容全部导入

    1. import * as obj from './03-按需导出.js'
    2. console.log(obj)
    3. /**
    4. * {
    5. * uname: 'zhangsan',
    6. * age: 20,
    7. * fn: function ......,
    8. * default: 'hello world' // 叫做default这个,是默认导出的内容
    9. * }
    10. */

    直接导入模块(无导出)

    如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。
    此时,可以直接导入并执行模块代码,示例代码如下:
    import '模块的路径'
    1. //xxx.js
    2. for (let i = 0; i < 10; i++) {
    3. console.log(i)
    4. }
    5. // 导入该模块
    6. import './xxx.js'
    实际的项目中,我们使用这种语法,导入css、less、图片等等资源

    导入内置模块和第三方模块

    ```javascript // 内置模块,支持默认导入 import fs from ‘fs’

// 内置模块,支持按需导入 import { readFile, writeFile } from ‘fs’

// 第三方模块,肯定支持默认导入(按需导入不一定支持,因为我们并不知道别人的模块是怎么写的) import dayjs from ‘dayjs’

  1. <a name="xje2x"></a>
  2. ## Promise
  3. Promise能够处理异步程序。Array/String/Date.......... 他们的级别是一样的
  4. <a name="r1yP0"></a>
  5. ### 回调地狱
  6. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22014993/1652015629161-b9585a40-a02c-4223-a41c-f4618faa1ea4.png#clientId=ua44f78a2-c0f3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=386&id=uc4d77e7a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=482&originWidth=1030&originalType=binary&ratio=1&rotation=0&showTitle=false&size=428983&status=done&style=none&taskId=u77b786a6-4654-48a8-a4f2-3f6942edaea&title=&width=824)<br />JS中或node中,都大量的使用了**回调函数**进行异步操作,而异步操作什么时候返回结果是不可控的,如果我们希望几个异步请求按照顺序来执行,那么就需要将这些异步操作嵌套起来,嵌套的层数特别多,就会形成**回调地狱** 或者叫做 **横向金字塔**。<br />下面的案例就有回调地狱的意思:<br />案例:有 a.txt、b.txt、c.txt 三个文件,使用fs模板按照顺序来读取里面的内容,代码:
  7. ```javascript
  8. // 将读取的a、b、c里面的内容,按照顺序输出
  9. const fs = require('fs');
  10. // 读取a文件
  11. fs.readFile('./a.txt', 'utf-8', (err, data) => {
  12. if (err) throw err;
  13. console.log(data.length);
  14. // 读取b文件
  15. fs.readFile('./b.txt', 'utf-8', (err, data) => {
  16. if (err) throw err;
  17. console.log(data);
  18. // 读取c文件
  19. fs.readFile('./c.txt', 'utf-8', (err, data) => {
  20. if (err) throw err;
  21. console.log(data);
  22. });
  23. });
  24. });

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

Promise简介

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

    Promise简单使用

    Promise是“承诺”的意思,实例中,它里面的异步操作就相当于一个承诺,而承诺就会有两种结果,要么完成了承诺的内容,要么失败。
    所以,使用Promise,分为两大部分,首先是有一个承诺(异步操作),然后再兑现结果。
    第一部分:定义“承诺”

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

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

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

    完整的代码:

    1. const fs = require('fs');
    2. // promise 承诺
    3. // 使用Promise分为两大部分
    4. // 1. 定义一个承诺
    5. let p = new Promise((resolve, reject) => {
    6. // resolve -- 解决,完成了; 是一个函数
    7. // reject -- 拒绝,失败了; 是一个函数
    8. // 异步操作的代码,它就是一个承诺
    9. fs.readFile('./files/a.txt', 'utf-8', (err, data) => {
    10. if (err) {
    11. reject(err);
    12. } else {
    13. resolve(data.length);
    14. }
    15. });
    16. });
    17. // 2. 兑现承诺
    18. // p.then(
    19. // (data) => {}, // 函数类似的参数,用于获取承诺成功后的数据
    20. // (err) => {} // 函数类型的参数,用于或承诺失败后的错误信息
    21. // );
    22. p.then(
    23. (data) => {
    24. console.log(data);
    25. },
    26. (err) => {
    27. console.log(err);
    28. }
    29. );

    then方法的链式调用

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

  • 前一个then里面返回的Promise对象,并且调用resolve的时候传递了数据,数据会被下一个then接收到
  • 前一个then里面如果没有调用resolve,则后续的then不会接收到任何值 ```javascript const { readFile } = require(‘fs’)

let p1 = new Promise((resolve, reject) => { readFile(‘./files/a.txt’, ‘utf-8’, (err, data) => { err ? reject(err) : resolve(data) }) })

let p2 = new Promise((resolve, reject) => { readFile(‘./files/b.txt’, ‘utf-8’, (err, data) => { err ? reject(err) : resolve(data) }) })

let p3 = new Promise((resolve, reject) => { readFile(‘./files/c.txt’, ‘utf-8’, (err, data) => { err ? reject(err) : resolve(data) }) })

// p.then().then().then()……………. // 前一个then如果返回一个Promise对象,Promise对象的结果会被下一个then得到

p1.then(res => { console.log(res.length) // a文件内容的长度 return p2 }) .then(res => { console.log(res.length) // b文件内容的长度 return p3 }) .then(res => { console.log(res.length) // c文件内容的长度 })

  1. > catch 方法可以统一获取错误信息
  2. <a name="QHX3W"></a>
  3. ### 使用第三方模块读取文件
  4. - `npm init -y`
  5. - `npm i then-fs` 安装then-fs模块
  6. - then-fs 内置的fs模块封装了,读取文件后,返回 Promise 对象,省去了我们自己封装
  7. ```javascript
  8. // then-fs 是一个第三方模块
  9. // 它把内置的 fs 模块重新封装了一下
  10. // 封装之后,每个方法都返回Promise对象
  11. // 这样的话,咱们就不要new Promise了,直接调用方法,就可以得到Promise对象了
  12. const { readFile } = require('then-fs')
  13. let p1 = readFile('./files/a.txt', 'utf-8')
  14. let p2 = readFile('./files/b.txt', 'utf-8')
  15. let p3 = readFile('./files/c.txt', 'utf-8')
  16. // 自己写then方法
  17. p1.then(res => {
  18. console.log(res.length)
  19. return p2
  20. }).then(res => {
  21. console.log(res.length)
  22. return p3
  23. }).then(res => {
  24. console.log(res.length)
  25. })

体会axios

  • npm i axios 使用之前,先下载安装
  • axios 基于Promise的网络请求库,支持浏览器环境和node环境 ```javascript const axios = require(‘axios’)

// 调用axios() axios.get() axios.post() 得到的就是Promise对象 let p = axios.get(‘http://www.itcbc.com:3006/api/getbooks‘)

p.then(res => { console.log(res.data.data) })

  1. <a name="TjzHB"></a>
  2. ### async 和 await 修饰符
  3. ES6 --- ES2015<br />async 和 await 是 ES2017 中提出来的。<br />异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题。<br />从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底。它们都有额外的复杂性,都需要理解抽象的底层运行机制。<br />异步I/O不就是读取一个文件吗,干嘛要搞得这么复杂?**异步编程的最高境界,就是根本不用关心它是不是异步。**<br />async 函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案。<br />ES2017 提供了async和await关键字。await和async关键词能够将异步请求的结果以返回值的方式返回给我们。
  4. ```javascript
  5. // async和await 代替了 then
  6. const { readFile } = require('then-fs')
  7. // 读取文件,并得到Promise对象
  8. let p1 = readFile('./files/a.txt', 'utf-8')
  9. let p2 = readFile('./files/b.txt', 'utf-8')
  10. let p3 = readFile('./files/c.txt', 'utf-8')
  11. // 使用async和await代替then拿结果
  12. async function abc() {
  13. // let 结果 = await Promise对象; //
  14. let res1 = await p1
  15. let res2 = await p2
  16. let res3 = await p3
  17. console.log(res2.length, res1.length, res3.length)
  18. }
  19. abc()

错误处理

先获取Promise对象

  1. const { readFile } = require('then-fs');
  2. let p1 = readFile('./files/a.txt', 'utf-8');
  3. let p2 = readFile('./files/bbbbb.txt', 'utf-8'); // 故意把这里的路径写错
  4. let p3 = readFile('./files/c.txt', 'utf-8');

获取结果,有两个方案,方案一,使用then获取,在链式的尾端,加入catch来进行错误处理

  1. // 获取结果,方案一 ------------- 使用then -----------
  2. p1.then(res => {
  3. console.log(res.length)
  4. return p2;
  5. }).then(res => {
  6. console.log(res.length);
  7. return p3;
  8. }).then(res => {
  9. console.log(res.length)
  10. }).catch(err => {
  11. console.log(err);
  12. });

获取结果,方案二,使用async和await获取结果,使用 try...catch... 来处理错误

  1. // 获取结果,方案二 --------- 使用async和await -----------
  2. async function abc() {
  3. try {
  4. // try 表示试试,试一试,尝试。这里放正常的代码
  5. let r1 = await p1;
  6. let r2 = await p2; // p2这里出错了,相当于 throw err,抛出错误。并且会终止这段代码执行
  7. let r3 = await p3;
  8. console.log(r1.length, r2.length, r3.length);
  9. return 1234
  10. } catch (e) {
  11. console.log(e); // e就是上面try里面,抛出的错误
  12. }
  13. }
  14. abc();

知识点:如果try里面没有错误,并且有 return 返回值,返回值相当于是 函数的返回值

使用axios体会错误处理的重要性

  1. const axios = require('axios')
  2. // 添加图书
  3. let p = axios.post('http://www.itcbc.com:3006/api/addbook', {
  4. bookname: 'a', // 添加图书,要求书名长度 2~10
  5. author: 'bcd',
  6. publisher: 'cde'
  7. })
  8. p.then(res => {
  9. console.log(res.data)
  10. }).catch(err => {
  11. console.log(err.response.data)
  12. })

三种状态

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

    当达到最终的 fulfilled 或 rejected 时,promise的状态就不会再改变了,结果也就固定了,不会再改变了

当调用 resolve的时候,Promise 将到达最终的状态。 达到最终状态之后,Promise的状态就不会再改变了。
多次调用 resolve 函数,只有第一次有效,其他的调用都无效。

  1. let p = new Promise((resolve, reject) => {
  2. resolve(1) // 调用resolve,表示Promise到达了成功的状态;一旦成功了,Promise的结果就不会再变了
  3. reject(2) // 无效
  4. resolve(3) // 无效
  5. setTimeout(() => {
  6. resolve(4) // 无效
  7. }, 1000)
  8. })
  9. p.then(
  10. res => {
  11. console.log(res)
  12. },
  13. err => {
  14. console.log(err)
  15. }
  16. )

同步异步?

  • new Promise里面的代码是立即执行的
  • 获取结果时(then方法时)是异步的,是最后执行的

    1. console.log(1);
    2. new Promise((resolve, reject) => {
    3. console.log(2);
    4. resolve();
    5. console.log(3);
    6. }).then(res => {
    7. console.log(4); // 这里是异步的
    8. })
    9. console.log(5);
    10. // 输出顺序: 1 2 3 5 4 ,因为只有 .then() 是异步的

    async和await补充

  • async 用于修饰一个 function

    • async 修饰的函数,总是返回一个 Promise 对象
    • 函数内的返回值,将自动包装在 resolved 的 promise 中
      • 比如async函数中 return 123
      • 相当于 return new Promise((resolve, reject) => resolve(123))
      • 如果希望返回失败状态的Promise对象,则 return Promise.reject(xxx)
  • await 只能出现在 async 函数内
    • await 让 JS 引擎等待直到promise完成并返回结果
    • 语法:let value = await promise对象; // 要先等待promise对象执行完毕,才能得到结果
    • 由于await需要等待promise执行完毕,所以await会暂停函数的执行,但不会影响函数外的同步任务 ```javascript // —————————— 单独看 async ——————————- // 1. async只能修饰函数 // 2. async函数如果有返回值,返回值将自动包装进Promise对象中 async function abc() { return 123 // return new Promise((resolve, reject) => { // resolve(123) // }) }

let res = abc() // console.log(res) // 得到Promise对象

// —————————— 单独看 await ——————————- // 1. await只能出现在async函数中 // 2. await下面的(不是后面的)代码,不会立即执行。要等到所有同步任务执行完,才能执行 console.log(1) async function fn() { console.log(2) await console.log(5) // await 会暂停函数的执行,使得await下面的代码在所有同步任务之后执行 console.log(3) } fn() console.log(4)

  1. <a name="jpQIv"></a>
  2. ### Promise的静态方法
  3. - Promise.resolve() --- 返回一个成功状态的Promise对象
  4. - Promise.reject() --- 返回一个失败状态的Promise对象
  5. ```javascript
  6. // Promise.resolve('hello world').then(res => {
  7. // console.log(res)
  8. // })
  9. // Promise.reject('错误信息').then(函数1, 函数2)
  10. Promise.reject('错误信息').then(
  11. res => {
  12. console.log('正确的结果是:', res)
  13. },
  14. err => {
  15. console.log('错误的结果是:', err)
  16. }
  17. )
  • Promise.all([ Promise对象, Promise对象, Promise对象 ]).then(res => {})
    • all是所有的意思,等所有的Promise对象都完成,会触发then,res包含所有的3个结果
  • Promise.race([ Promise对象, Promise对象, Promise对象 ]).then(res => {})
    • race是比赛、赛跑的意思,所以无论哪一个Promise对象得到结果,就算完成了 ```javascript const { readFile } = require(‘then-fs’) let p1 = readFile(‘./files/a.txt’, ‘utf-8’) let p2 = readFile(‘./files/b.txt’, ‘utf-8’) let p3 = readFile(‘./files/c.txt’, ‘utf-8’)

// all是所有的意思 // Promise.all([p1, p2, p3]).then(res => { // console.log(res) // [‘aaaaaa’, ‘bbbbbbbbbbbbbbbbbbbbbbbbbb’, ‘cccc’] // })

// race是赛跑的意思 Promise.race([p1, p2, p3]).then(res => { console.log(res) // aaaaaa 或 bbbbbbbb 或 ccccccccc })

  1. <a name="tbnxM"></a>
  2. ### 小结
  3. - Promise是异步任务的一种解决方案
  4. - 在避免回调地狱的基础上,可以获取到异步任务的结果
  5. - 多个异步任务同时执行,并且还要保证顺序的情况下,非常适合使用Promise.
  6. **如何使用?**
  7. - 第一步:得到Promise对象
  8. - 自己 new Promise() ----- 很少用
  9. - 通过第三方模块实现,比如 then-fs(调用readFile直接得到Promise对象) 、比如 axios(axios.post()直接得到Promise对象)
  10. - 第二步:获取结果
  11. - Promise对象.then(res => {})
  12. - async和await
  13. ```javascript
  14. (async function () {
  15. let res = await Promise对象;
  16. })();

其他小点

  • 调用 resolve() 或者 reject() ,表示Promise到达了最终状态,并且值不会再改变了
  • then() 和 await 是异步的。new Promise的时候,是同步的。
    • new Promise(() => { /*这里的代码是立即执行的*/ })
    • then(res => { /* 这是代码是异步的 */ })
    • await 下面的 (不是后面的)所有代码都是异步的
  • 链式调用then方法:前一个then返回Promise对象的话,结果会被下一个then得到
  • async函数如果有返回值,相当于函数返回了Promise对象,原来的返回值会被包装进Promise对象

处理 JSON数据练习

准备工作

  1. 安装所需的包

npm init
npm i then-fs

  1. 创建 books.json,代码如下

    1. [
    2. { "bookname": "史记", "author": "司马迁", "publisher": "北京出版社", "id": 3 },
    3. { "bookname": "遮天", "author": "辰东", "publisher": "仙侠出版社", "id": 5 },
    4. { "bookname": "西游记", "author": "吴承恩", "publisher": "北京出版社", "id": 6 },
    5. { "bookname": "JS权威指南", "author": "王宁", "publisher": "上海出版社", "id": 9 }
    6. ]
  2. 创建 json-parser.js,封装4个函数,并导出 ```javascript // 这里封装 query、add、del、update方法,分别查询JSON数据、添加、修改、删除数据 const { join } = require(‘path’) const { readFile, writeFile } = require(‘then-fs’)

// 拼接books.json的绝对路径 const filename = join(__dirname, ‘books.json’)

function query() {} function add() {} function del() {} function update() {}

module.exports = { query, add, del, update }

  1. 4. 创建 test.js 测试文件,导入 json-parser.js ,分别调用4个函数进行测试
  2. ```javascript
  3. // 导入 json-parser.js ,测试里面封装的四个函数
  4. const { query, add, del, update } = require('./json-parser')
  5. // 1. 调用query测试
  6. query()

query方法

  1. async function query() {
  2. // try、catch里面的返回值,相当于是外层函数的返回值
  3. try {
  4. // let 结果 = await Promise对象
  5. let res = await readFile(filename, 'utf-8') // 得到Promise对象
  6. let arr = JSON.parse(res)
  7. // console.log(arr) // 这里的输出,仅仅是为了测试
  8. return arr
  9. } catch (err) {
  10. return Promise.reject(err)
  11. }
  12. }

test.js 中,调用函数,进行测试

  1. // 导入 json-parser.js ,测试里面封装的四个函数
  2. const { query, add, del, update } = require('./json-parser')
  3. // 1. 调用query测试
  4. query()
  5. .then(res => {
  6. console.log(res)
  7. })
  8. .catch(err => {
  9. // 所有的错误对象,都有 message 属性,表示错误描述信息
  10. console.log('错误信息是:', err.message)
  11. })

add方法

  1. async function add(obj) {
  2. try {
  3. // 1. 先获取全部的图书,得到数组
  4. let arr = await query() // query()返回Promise对象
  5. // console.log(arr)
  6. // 2. 把新书加进去
  7. obj.id = arr[arr.length - 1].id + 1
  8. arr.push(obj)
  9. // 3. 把全部的图书重新存起来
  10. return writeFile(filename, JSON.stringify(arr))
  11. } catch (err) {
  12. return Promise.reject(err)
  13. }
  14. }

test.js 中,调用函数,测试

  1. // 2. 测试add方法
  2. add({ bookname: '三国演义', author: '罗贯中', publisher: '北京出版社' })
  3. .then(res => {
  4. console.log(res) // 输出undefined,说明写入成功
  5. })
  6. .catch(err => {
  7. console.log(err.message)
  8. })

del方法

  1. async function del(id) {
  2. try {
  3. // 1. 先获取全部的图书
  4. let arr = await query()
  5. // 2. 根据id筛选,把一本书删除掉
  6. let result = arr.filter(item => item.id != id)
  7. // 3. 把剩余的图书存起来
  8. return writeFile(filename, JSON.stringify(result))
  9. } catch (err) {
  10. return Promise.reject(err)
  11. }
  12. }

test.js 中测试

  1. // 3. 调用del方法
  2. del(9)
  3. .then(res => {
  4. console.log(res) // 输出undefined,说明没有错误
  5. })
  6. .catch(err => {
  7. console.log(err)
  8. })

update方法

  1. async function update(obj) {
  2. try {
  3. // 1. 获取全部图书
  4. let arr = await query()
  5. // 2. 修改一本书
  6. let index = arr.findIndex(item => item.id == obj.id)
  7. arr.splice(index, 1, obj)
  8. // 3. 把全部图书重新存起来
  9. return writeFile(filename, JSON.stringify(arr))
  10. } catch (err) {
  11. return Promise.reject(err)
  12. }
  13. }

test.js 中进行测试:

  1. // 4. 调用update方法进行测试
  2. update({ bookname: '朝花夕拾', author: '鲁迅', publisher: '北京出版社', id: 6 })
  3. .then(res => {
  4. console.log(res) // 输出undefined,说明没有错误
  5. })
  6. .catch(err => {
  7. console.log(err)
  8. })

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

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)是宏任务中的一个部分,它的执行时机是在同步代码执行之后,下一个宏任务执行之前。
微任务包含

  1. Promise.then
  2. await

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

运行机制

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

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

image.png
image.png

面试题分析

  • 1

    1. console.log(1)
    2. setTimeout(function() {
    3. console.log(2)
    4. }, 0)
    5. const p = new Promise((resolve, reject) => {
    6. resolve(1000)
    7. })
    8. p.then(data => {
    9. console.log(data)
    10. })
    11. console.log(3)
  • 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. })
    26. console.log(10)
  • 3

    1. console.log(1)
    2. setTimeout(function() {
    3. console.log(2)
    4. }, 0)
    5. const p = new Promise((resolve, reject) => {
    6. console.log(3)
    7. resolve(1000) // 标记为成功
    8. console.log(4)
    9. })
    10. p.then(data => {
    11. console.log(data)
    12. })
    13. console.log(5)
  • 4

    1. new Promise((resolve, reject) => {
    2. resolve(1)
    3. new Promise((resolve, reject) => {
    4. resolve(2)
    5. }).then(data => {
    6. console.log(data)
    7. })
    8. }).then(data => {
    9. console.log(data)
    10. })
    11. console.log(3)
  • 5

    1. setTimeout(() => {
    2. console.log(1)
    3. }, 0)
    4. new Promise((resolve, reject) => {
    5. console.log(2)
    6. resolve('p1')
    7. new Promise((resolve, reject) => {
    8. console.log(3)
    9. setTimeout(() => {
    10. resolve('setTimeout2')
    11. console.log(4)
    12. }, 0)
    13. resolve('p2')
    14. }).then(data => {
    15. console.log(data)
    16. })
    17. setTimeout(() => {
    18. resolve('setTimeout1')
    19. console.log(5)
    20. }, 0)
    21. }).then(data => {
    22. console.log(data)
    23. })
    24. console.log(6)
  • 6

    1. <!-- 如果有多个script,则优先执行script,再执行定时器 -->
    2. <script>
    3. console.log(1);
    4. async function fnOne() {
    5. console.log(2);
    6. await fnTwo(); // 右结合先执行右侧的代码, 然后等待
    7. console.log(3);
    8. }
    9. async function fnTwo() {
    10. console.log(4);
    11. }
    12. fnOne();
    13. setTimeout(() => {
    14. console.log(5);
    15. }, 2000);
    16. let p = new Promise((resolve, reject) => { // new Promise()里的函数体会马上执行所有代码
    17. console.log(6);
    18. resolve();
    19. console.log(7);
    20. })
    21. setTimeout(() => {
    22. console.log(8)
    23. }, 0)
    24. p.then(() => {
    25. console.log(9);
    26. })
    27. console.log(10);
    28. </script>
    29. <script>
    30. console.log(11);
    31. setTimeout(() => {
    32. console.log(12);
    33. let p = new Promise((resolve) => {
    34. resolve(13);
    35. })
    36. p.then(res => {
    37. console.log(res);
    38. })
    39. console.log(15);
    40. }, 0)
    41. console.log(14);
    42. </script>