用生成器替代迭代器
// 原先用迭代器实现: function createArrayIterator(arr) { let index = 0 return { next: () => { if (index < arr.length) { return { done: false, value: arr[index++] } } else { return { done: true, value: undefined } } } } }
因为生成器是特殊的迭代器,所以我们可以用生成器去简化代码:
function* createArrayIterator(arr) { for (const item of arr) { yield item } }
yield*
yield*是一种yield的语法糖,可以用它来生产一个可迭代对象
如下面的 yield* arr ,这个代码会依次迭代 arr 这个可迭代对象,每次迭代其中的一个值
function* createArrayIterator(arr) { yield* arr }
async-await
async异步函数
async 关键字用于声明一个异步函数
// 写法一: async function foo1() { } // 写法二: const foo2 = async () => { } // 写法三: class Foo { async bar() { } }
async异步函数与普通函数的区别
1. 返回值
async 函数一定会返回一个 promise 对象。
如果一个 async 函数的返回值看起来不是 promise,那么它将会被隐式地包装在一个 promise 中。
2. 异常
普通函数:
function foo() { console.log("foo function start~") console.log("中间代码~") throw new Error("error message") console.log("foo function end~") } foo().catch(err => { console.log("error message:", err) }) console.log("后续还有代码~~~~~") // 输出抛出异常前面的代码并抛出异常,后面代码不执行
异步函数:
async function foo() { console.log("foo function start~") console.log("中间代码~") // 异步函数中的异常, 会被作为异步函数返回的Promise的reject值的 throw new Error("error message") console.log("foo function end~") } foo().catch(err => { console.log("error message:", err) }) console.log("后续还有代码~~~~~") // 后续代码会被执行,之后打印错误信息
如果我们在async中抛出了异常,那么程序它并不会像普通函数一样报错,而是会作为Promise的reject来传递;
await关键字
await 表达式会暂停整个 async 函数的执行进程并出让其控制权,只有当其等待的基于 promise 的异步操作被兑现或被拒绝之后才会恢复进程。promise 的解决值会被当作该 await 表达式的返回值。
注意:await关键字只在 async 函数内有效。如果你在 async 函数体之外使用它,就会抛出语法错误。
async/await搭配使用:
function requestData1() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(1111) }, 2000); }) } function requestData2() { return new Promise((resolve, reject) => { setTimeout(() => { reject(222) }, 3000); }) } async function foo1() { // 1.await跟上表达式 const res1 = await requestData1() // 2. await跟上其他值 const res2 = await 123 // "await" 对此表达式的类型没有影响 console.log("后面的代码1", res1) console.log("后面的代码2", res2) } foo1() // 3.reject值 async function foo2() { const res3 = await requestData2() console.log("res3:", res3) } foo2().catch(err => { console.log("err:", err) })
当await后面跟着是reject值,则它返回的值会作为整个foo2返回的Promise值,await之后的代码不会执行
总结:
async/await的目的为了简化使用基于 promise 的 API 时所需的语法(promise语法糖)。
async/await的行为就好像搭配使用了生成器和 promise。
事件循环
进程和线程
操作系统的工作方式
浏览器中的JavaScript线程
JavaScript是单线程的,也就是说同一时刻只能运行一行代码
浏览器的事件循环
如果在执行JavaScript代码的过程中,有异步操作呢?
- 中间我们插入了一个setTimeout的函数调用;
- 这个函数被放到入调用栈中,执行会立即结束,并不会阻塞(JS线程中)后续代码的执行;
那么,传入的一个函数(比如我们称之为timer函数),会在什么时候被执行呢?
- 事实上,setTimeout是调用了web api,在合适的时机,会将timer函数加入到一个事件队列中;
- 事件队列中的函数,会被放入到调用栈中,在调用栈中被执行;
简单图解
浏览器的宏任务和微任务
但是事件循环中并非只维护着一个队列,事实上是有两个队列:
- 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
- 微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等
那么事件循环对于两个队列的优先级是怎么样的呢?
- main script中的代码优先执行(编写的顶层script代码);
- 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
- 也就是宏任务执行之前,必须保证微任务队列是空的;
- 如果不为空,那么就优先执行微任务队列中的任务(回调);
Node的事件循环
Node的架构分析(理解)
- 我们会发现libuv中主要维护了一个EventLoop和worker threads(线程池);
- EventLoop负责调用系统的一些其他操作:文件的IO、Network、child-processes等
- libuv是一个多平台的专注于异步IO的库,它最初是为Node开发的,但是现在也被使用到Luvit、Julia、pyuv等其他地方;
Node的事件循环的阶段
事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道
- 无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中;
- 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;
Node相较于浏览器要维护的东西很多,浏览器主要维护两个队列(简单划分为两个队列),而Node事件循环会划分Tick来执行
一次完整的事件循环Tick分成很多个阶段:
- 定时器(Timers):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
- 待定回调(Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED(连接拒绝错误)。
- idle, prepare:仅系统内部使用。
- 轮询(Poll):检索新的 I/O 事件;执行与 I/O 相关的回调; (JS引擎在没有其他任务要执行时会停留在轮询的阶段检查有没有IO,一旦有IO,则下一次就从这里开始执行IO)
- 检测(check):setImmediate() 回调函数在这里执行。
- 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)
Node的事件循环图解:
Node的微任务和宏任务
我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:
- 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;
- 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;
但是,Node中的事件循环不只是 微任务队列和 宏任务队列:
- 微任务队列又细分为:
- next tick queue:process.nextTick;
- other queue:Promise的then回调、queueMicrotask;
- 宏任务队列又细分为:
- timer queue:setTimeout、setInterval;
- poll queue:IO事件;
- check queue:setImmediate;
- close queue:close事件;
所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:
- next tick microtask queue;
- other microtask queue;
- timer queue;
- poll queue;
- check queue;
- close queue;
JavaScript模块化
错误处理方案
函数出现错误处理
通过throw
抛出错误信息并终止程序,强制调用者修改错误
/** * 如果我们有一个函数, 在调用这个函数时, 如果出现了错误, 那么我们应该是去修复这个错误. */ function sum(num1, num2) { // 当传入的参数的类型不正确时, 应该告知调用者一个错误 if (typeof num1 !== "number" || typeof num2 !== "number") { // return undefined throw "parameters is error type~" } return num1 + num2 } // 调用者(如果没有对错误进行处理, 那么程序会直接终止) // console.log(sum({ name: "why" }, true)) console.log(sum(20, 30)) console.log("后续的代码会继续运行~")
抛出异常的补充
抛出异常的分类有:
- 抛出一个字符串类型(基本的数据类型):
throw "error"
- 比较常见的是抛出一个对象类型:
throw { errorCode: -1001, errorMessage: "type不能为0~" }
- 创建类, 并且创建这个类对应的对象:
throw new HYError(-1001, "type不能为0~")
- 提供了一个Error(默认创建出来会有很多信息,了解即可)
Error包含三个属性:
- messsage:创建Error对象时传入的message;
- name:Error的名称,通常和类的名称一致;
- stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack;
Error有一些自己的子类:
- RangeError:下标值越界时使用的错误类型;
- SyntaxError:解析语法错误时使用的错误类型;
- TypeError:出现类型错误时,使用的错误类型;
const err = new Error("type不能为0")
// 可以修改error的名称和栈,但一般不会修改
err.name = "why"
err.stack = "aaaa"
- Error的子类:
const err = new TypeError("当前type类型是错误的~")
如果函数中已经抛出了异常, 那么后续的代码都不会继续执行了
处理抛出的异常
两种处理方法:
- 第一种是不处理, 那么异常会进一步的抛出, 直到最顶层的调用
如果在最顶层也没有对这个异常进行处理, 那么我们的程序就会终止执行, 并且报错
- 使用try catch来捕获异常
function foo(type) { if (type === 0) { throw new Error("foo error message~") } } // 1.不处理, bar函数会继续将收到的异常直接抛出去 function bar() { foo(0) } // test 拿到 bar 抛出的异常 function test() { // 2. 在上层使用try catch来捕获异常也是可以的 try { bar() } catch (error) { console.log("error:", error) } } function demo() { test() } // 有对异常进行处理就会继续执行后续代码 console.log("后续的代码执行~") 补充(finally语法解释):不管有没有发生异常,finally里面的代码都会执行 try { } catch (err) { }finally { }
什么是模块化
那么,到底什么是模块化开发呢?
- 事实上模块化开发最终的目的是将程序划分成一个个小的结构;
- 这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构;
- 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用;
- 也可以通过某种方式,导入另外结构中的变量、函数、对象等;
// 导出 exports = {sum,add,sub} // 导入 const {sum,add} = require('abc.js')
上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程;
CommonJS规范和Node关系
我们需要知道CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了 体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS。
- Node是CommonJS在服务器端一个具有代表性的实现;
- Browserify是CommonJS在浏览器中的一种实现; (很少用了)
- webpack打包工具具备对CommonJS的支持和转换;
所以,Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
- 在Node中每一个js文件都是一个单独的模块;
- 这个模块中包括CommonJS规范的核心变量:exports、module.exports、require;
- 我们可以使用这些变量来方便的进行模块化开发;
前面我们提到过模块化的核心是导出和导入,Node中对其进行了实现:
- exports和module.exports可以负责对模块中的内容进行导出;
- require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
案例时间:
1. exports导出:
注意:exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出;
另外一个文件中可以导入:
上面这行完成了什么操作呢?理解下面这句话,Node中的模块化一目了然
- 意味着main中的bar变量等于exports对象;
- 也就是require通过各种查找方式,最终找到了exports这个对象;
- 并且将这个exports对象赋值给了bar变量;
- bar变量就是exports对象了;
Node中实现CommonJS的本质就是对象的引用赋值
深入:module.exports
其实导出的真正实现者的是module.exports,只是官方默认module.exports=exports
一旦module.exports修改了对象,则导出的对象就与exports无关了
2. require细节
require是一个函数,可以帮助我们引入一个文件(模块)中导入的对象。
比较常见的查找规则
导入格式如下:require(X)
情况一:X是一个核心模块,比如path、http
- 直接返回核心模块,并且停止查找
情况二:X是以 ./ 或 ../ 或 /(根目录)开头的 (在本地目录中查找)
第一步:将X当做一个文件在对应的目录下查找;
- 1.如果有后缀名,按照后缀名的格式查找对应的文件
- 2.如果没有后缀名,会按照如下顺序:
- 1> 直接查找文件X
- 2> 查找X.js文件
- 3> 查找X.json文件
- 4> 查找X.node文件
第二步:没有找到对应的文件,将X作为一个目录
查找目录下面的index文件
- 1> 查找X/index.js文件
- 2> 查找X/index.json文件
- 3> 查找X/index.node文件
如果没有找到,那么报错:not found
情况三:直接是一个X(没有路径),并且X不是一个核心模块
/Users/coderwhy/Desktop/Node/TestCode/04_learn_node/05_javascript-module/02_commonjs/main.js中编写require('why’)
path是查找路径(上图可以由console.log(module)得到path)
如果上面的路径中都没有找到,那么报错:not found
3. 模块的加载过程
结论一:模块在被第一次引入时,模块中的js代码会被运行一次
扩: 加载过程是同步的
结论二:模块被多次引入时,会缓存,最终只加载(运行)一次
- 为什么只会加载运行一次呢?
- 这是因为每个模块对象module都有一个属性:loaded。
- 为false表示还没有加载,为true表示已经加载;
结论三:如果有循环引入,那么加载顺序是什么?
如果出现下图模块的引用关系,那么加载顺序是什么呢?
这个其实是一种数据结构:图结构;
图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);
Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb
4. CommonJS缺点
5. CMD规范(了解即可)
CMD规范也是应用于浏览器的一种模块化规范:
- CMD 是Common Module Definition(通用模块定义)的缩写;
- 它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来;
- 但是目前CMD使用也非常少了;
CMD也有自己比较优秀的实现方案:
- SeaJS
SeaJs
第一步:下载SeaJS
- 下载地址:https://github.com/seajs/seajs
- 找到dist文件夹下的sea.js
第二步:引入sea.js和使用主入口文件
- seajs是指定主入口文件的