node非阻塞模型
node运行机制的核心可以概括为非阻塞 ,我们都知道javascript
是单线程的,不单单是指在浏览器环境中,在node
环境中同样如此。
注:这里明确js的单线程的含义------在浏览器环境中,一个tab页对应一个renderer渲染进程,这个进程又包括了如GUI渲染线程、js引擎线程、计时器线程等多个线程,所以对于浏览器来说,js代码是由一个js引擎线程来执行的,即js是单线程的;在node环境下,一个node应用对应了一个进程,这个进程下负责执行js代码的也只有一个线程,即node环境下js也是单线程的。
简单举个单线程阻塞的例子:
javascript
function a() {
console.log('执行a函数');
}
function b() {
while(true) {} // 无限循环阻塞了整个js执行线程的继续执行,a函数永远得不到执行
a();
}
b();
node
环境中的js
之所以可以应对高并发的耗时操作(比如请求响应、数据库操作、IO)主要就归功于node的非阻塞机制。
所谓非阻塞运行机制,概括来说就是将高耗时的操作"移出"js主线程(做异步处理,即交给异步模块处理),这样也就做到了不阻塞js主线程不断向下处理逻辑。 而那些高耗时的操作(相对于js主线程)后台执行,他们被异步模块所处理,异步模块又是多线程的,换句话说还是多线程处理高并发。
当js主线程的调用栈清空时,后台处理的异步操作按照一定的优先级顺序加入js主线程的调用栈来执行。这个优先级顺序不同于浏览器环境下的事件循环规则,而是node事件循环。
事件循环说白了就是js代码执行的一种机制,这种执行机制叫做"事件循环"。
node事件循环
简单回顾一下浏览器环境中的事件循环是什么样子的,如下demo:
javascript
setTimeout(() => {
console.log('callback');
}, 1000)
console.log("begin");
- 首先整个
Task
入栈(被js主线程执行),遇到setTimeout
,将其交给异步模块处理(计时器线程)。
- 此时js主线程继续执行输出了
begin
,与js主线程并行的还有计时器线程中的计时任务,直至1s后,计时器线程将setTimeout
的回调函数推入"宏任务队列"中去。 - js引擎线程(主线程)从宏任务队列中取出队头回调,输出
callback
。
上面我们提到了"宏任务队列",当然还有"微任务队列",两者收集不同的回调并在不同时机执行,这里不多赘述。node事件循环与浏览器事件循环做对比,其实就两点差异:
-
宏任务队列一分为三:
Timer
队列、Poll
队列、Check
队列,宏任务队列的本质不变,都是在js引擎线程空时从队列中取出新的回调入栈执行,只是说这三者对异步操作进行了更细的分类,分别收集不同异步操作的回调。Timer队列 :收集
setTimeout
与setInterval
这两个计时器的回调任务Poll队列 :收集
文件IO
、数据库操作
、网络请求
等耗时操作的回调(出队优先级高于其它两个队列)Check队列 :收集
setImmediate
(node环境特有的api,也就是把回调立即加入队列)的回调。 -
在微任务队列之前增加
nextTick
队列,收集process.nextTick
的回调函数。(从宏任务队列中取出新的回调入栈执行的过程叫做一次
Tick
,nextTick
就是在下一次Tick
之前执行)
如上示意图,js调用栈空时接着执行nextTick
队列中的任务(入栈执行),再者是微任务,最后在事件循环队列中取出下一个回调执行。
demo
javascript
console.log('同步代码');
process.nextTick(() => {
console.log('tick');
});
Promise.resolve().then(() => {
console.log('promise');
});
setImmediate(() => {
console.log('setImmediate');
})
// 同步代码 tick promise setImmediate
除了第一行直接输出之外,其它的都是异步任务,进入对应的队列,然后按顺序执行。