Event Loop的理解
JavaScript 是单线程的,当主线程遇到耗时的I/O操作,就会把把这些操作尽量转移给操作系统来执行,而操作系统是多线程(同理,浏览器中的js也是单线程的,只不过浏览器是多线程的)。nodejs的底层libuv使用线程池来处理这些异步的任务,当任务处理完成后,操作系统会通知nodejs,nodejs就会把对应的回调函数添加到poll轮训队列中,nodejs处理完主线程的任务后,就会从轮训队列中执行对应的回调函数。
event loop 执行完毕的情况:
- 队列的操作全被执行完了
- 执行的回调数目到达指定的最大值
nodejs轮询阶段示意图:
- timers 执行: setTimeout 和 setInterval 的回调函数
- I/O callbacks: 不在 timers 阶段、close callbacks 阶段和 check 阶段这三个阶段执行的回调,都由此阶段负责,这几乎包含了所有回调函数
- idle, prepare: event loop内部阶段,我们暂不需要了解
- poll: 获取新的 I/O 事件
- check: 执行 setImmediate
- close callback: 执行关闭事件的回调函数,如 mongoose.on('close', fn) 里的 fn
异步相关的理解
promise
-
promise的作用:避免了回调地狱,将嵌套的回调形式,变成了链式的调用,调高了阅读性,降低了代码编写难度。
-
promise的自行实现:promise的简单实现可以参考这篇文章
-
promise的原理详解:异步事件管理:从Promise与async/await到RxJS
-
promise的三种状态:fulfilled、rejected、pending
- 不管是resolve还是reject,Promise.prototype.then()和Promise.prototype.catch()方法会返回一个全新的Promise对象(finally()不会返回promise),因此它们之间可以进行链式调用。一旦一个Promise调用环节出现了错误,被catch捕获,则后续的promise则不会被调用。
- promise的执行
new Promsie((resolve,reject)=>{
console.log('1');
setTimeout(()=>{
resolve('2');
},2000);
console.log('3')
}).then(res=>{
console.log(res);
});
执行结果:
1
3
一秒钟后
2
解释:promise在创建后,不管有没有调用其then方法,promise都会执行,并保存执行结果,此后不管再调用多少次then方法,其结果不变。
复制代码
- promise.all()实现请求超时处理 promise.all()可以来实现并发操作,这时可以定义一个五秒钟后必定失败的promise,这样就可以实现请求超时处理了
- Promise的构造函数以及then()中执行的函数都可以认为是在try...catch块中运行,因此即便使用了throw,程序本身也不会因为抛出异常而终止。
generator
-
形式上:
Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield语句,定义不同的内部状态
-
调用方式:
enerator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,我们可以通过调用 next 方法,使得指针移向下一个状态
-
通过next函数传值:
这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。这个用处在其用来实现 async/await 至关重要。
async/await
- generator函数自动执行的语法糖
- 有配合promise和不配合promise两种实现async/await的方法
process常见考点
1. setImmediate、setTimeOut、setInterval、process.nextTick的区别
在轮询阶段:
- 在整个轮询的开始执行process.nextTick
- 然后再执行setTimeOut、setInterval
- 再执行其他的回调函数
- 最后执行setImmediate
process.nextTick()的理解
process.nextTick()方法将 callback 添加到"next tick 队列"。一旦当前事件轮询队列的任务全部完成,在nexttick队列中的所有callbacks会被依次调用。
nextTick更加有效率。事件轮询随后的ticks调用,会在任何I/O事件(包括定时器)之前运行。
每次事件轮询后,在额外的I/O执行前,next tick队列都会优先执行。 递归调用nextTick callbacks 会阻塞任何I/O操作,就像一个while(true); 循环一样。
nodejs如何利用多核
-
cluster模块
-
child_process模块
-
利用pm2,fork或者cluster模式
Buffer
nodejs的重点就是文件(fs)和网络部分(http),但是这两部分都是依赖stream,而stream都使用了Buffer模块。
概念:
- TypedArray:读取或者操作二进制数据流的机制
- Buffer:Buffer 类以一种更优化和适用于 NodeJS 操作的方式实现了 Unit8Array API。总而言之,Buffer 类是用来处理二进制数据,因为太常用了,所以直接放在了全局变量里,使用的时候无需 require。
Buffer 类的实例类似于整型数组,不过缓冲区的大小在创建时确定,不能调整。Buffer 对象不同之处在于它不经 V8 的内存分配机制,Buffer 是一个 JavaScript 和 C++ 结合的模块,内存由 C++ 申请,JavaScript 分配。
使用:
- Buffer.from()
- Buffer.alloc()
- Buffer.allocUnsafe()
详细使用请参考:NodeJS stream 一:Buffer
stream
参考:
概念:
-
从术语上讲流是对输入输出设备的抽象
-
从程序角度而言流是有方向的数据,按照流动方向可以分为三种流:
- 设备流向程序:readable(readable)
- 程序流向设备:writable
- 双向:duplex、transform
-
按照 Unix 的哲学:一切皆文件,在 NodeJS 中对文件的处理多数使用流来完成
- 普通文件
- 设备文件(stdin、stdout)
- 网络文件(http、net)
-
在 NodeJS 中所有的 Stream 都是 EventEmitter 的实例
使用:
const fs = require('fs');
const FILEPATH = '...';
const rs = fs.createReadStream(FILEPATH);
const ws = fs.createWriteStream(DEST);
rs.pipe(ws);
复制代码
数据必须是从上游 pipe 到下游,也就是从一个 readable 流 pipe 到 writable 流。
流的两种工作方式:
- 流动模式:数据由底层系统读出,并尽可能快地提供给应用程序
- 暂停模式:必须显示地调用 read() 方法来读取若干数据块
流在默认状态下是处于暂停模式的,也就是需要程序显式的调用 read() 方法,可我们的例子中并没有调用就可以得到数据,因为我们的流通过 pipe() 方法切换成了流动模式,这样我们的 _read() 方法会自动被反复调用,直到数据读取完毕,所以我们每次 _read() 方法里面只需要读取一次数据即可。
流动模式和暂停模式切换:
-
暂停模式到流动模式:
- 通过添加 data 事件监听器来启动数据监听
- 调用 resume() 方法启动数据流
- 调用 pipe() 方法将数据转接到另一个 可写流
-
流动模式切换为暂停模式:
- 在流没有 pipe() 时,调用 pause() 方法可以将流暂停
- pipe() 时,需要移除所有 data 事件的监听,再调用 unpipe() 方法
流的事件:
- data:触发流为流动模式
- end:数据处理完成后会触发一个 end 事件
- error:数据处理过程中出现了错误会触发 error 事件
- readable:NodeJS 为我们提供了一个 readable 的事件,事件在可读流准备好数据的时候触发,也就是先监听这个事件,收到通知又数据了我们再去读取就好了
开始使用流动模式的时候我经常会担心一个问题,上面代码中可读流在创建好的时候就生产数据了,那么会不会在我们绑定 readable 事件之前就生产了某些数据,触发了 readable 事件,我们还没有绑定,这样不是极端情况下会造成开头数据的丢失嘛
可事实并不会,按照 NodeJS event loop 我们创建流和调用事件监听在一个事件队列里面,儿生产数据由于涉及到异步操作,已经处于了下一个事件队列,我们监听事件再慢也会比数据生产块,数据不会丢失。