likes
comments
collection
share

node运行机制&事件循环

作者站长头像
站长
· 阅读数 8

node非阻塞模型

node运行机制的核心可以概括为非阻塞,我们都知道javascript是单线程的,不单单是指在浏览器环境中,在node环境中同样如此。

注:这里明确js的单线程的含义——在浏览器环境中,一个tab页对应一个renderer渲染进程,这个进程又包括了如GUI渲染线程、js引擎线程、计时器线程等多个线程,所以对于浏览器来说,js代码是由一个js引擎线程来执行的,即js是单线程的;在node环境下,一个node应用对应了一个进程,这个进程下负责执行js代码的也只有一个线程,即node环境下js也是单线程的。

简单举个单线程阻塞的例子:

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:

setTimeout(() => {
    console.log('callback');
}, 1000)

console.log("begin");
  1. 首先整个Task入栈(被js主线程执行),遇到setTimeout,将其交给异步模块处理(计时器线程)。
  1. 此时js主线程继续执行输出了begin,与js主线程并行的还有计时器线程中的计时任务,直至1s后,计时器线程将setTimeout的回调函数推入”宏任务队列“中去。
  2. js引擎线程(主线程)从宏任务队列中取出队头回调,输出callback

上面我们提到了”宏任务队列“,当然还有”微任务队列“,两者收集不同的回调并在不同时机执行,这里不多赘述。node事件循环与浏览器事件循环做对比,其实就两点差异:

  • 宏任务队列一分为三:Timer队列、Poll队列、Check队列,宏任务队列的本质不变,都是在js引擎线程空时从队列中取出新的回调入栈执行,只是说这三者对异步操作进行了更细的分类,分别收集不同异步操作的回调。

    Timer队列:收集setTimeoutsetInterval这两个计时器的回调任务

    Poll队列:收集文件IO数据库操作网络请求等耗时操作的回调(出队优先级高于其它两个队列

    Check队列:收集setImmediate(node环境特有的api,也就是把回调立即加入队列)的回调。

  • 在微任务队列之前增加nextTick队列,收集process.nextTick的回调函数。

    (从宏任务队列中取出新的回调入栈执行的过程叫做一次TicknextTick就是在下一次Tick之前执行)

node运行机制&事件循环

如上示意图,js调用栈空时接着执行nextTick队列中的任务(入栈执行),再者是微任务,最后在事件循环队列中取出下一个回调执行。

demo

console.log('同步代码');

process.nextTick(() => {
    console.log('tick');
});

Promise.resolve().then(() => {
    console.log('promise');
});

setImmediate(() => {
    console.log('setImmediate');
})

// 同步代码 tick promise setImmediate

除了第一行直接输出之外,其它的都是异步任务,进入对应的队列,然后按顺序执行。