乐趣区

关于前端:nodejs中事件循环机制与面试题详解

nodejs 中架构如下图所示,通过 v8 引擎来执行 js 代码,通过中间层 libuv 来读写文件系统、网络等做一些操作。

nodejs 中提供阻塞和非阻塞的调用形式,比方 fs 模块中读取文件,能够依据须要应用 readFile(异步)或者 readFileSync(同步)。

如果应用同步的编程形式,那么后续代码的执行须要等到此次执行完结,程序的运行会被“阻塞”。也能够应用异步的编程形式,后续代码的执行无需期待此次的执行完结,不会“阻塞”程序的运行,但它同样也会存在一个问题,那就是非阻塞式调用须要一直的轮询获取异步调用的执行后果。

libuv 中的 io 操作就是应用的”非阻塞式调用“,但它如果一直轮询获取后果这个过程对系统有肯定的性能影响,为了将这个影响升高,libuv 中将一直轮询的这个过程放到“线程池”当中,当轮询到后果时,将对应的回调和获取的后果搁置到 event loop(事件循环)的某一个队列中,事件循环再来进行下一步的操作,通过 javascript 来执行回调。

nodejs 中的事件循环要比 javascript 的事件循环更为简单一些,一次循环分为以下几个阶段

* 定时器 (timer): 在这个阶段执行 setTimeout、setInterval 的回调函数
* 待定回调 (pending callbacks):某些零碎操作(如 TCP 谬误类型)执行回调
* idle, prepare:仅零碎外部应用。* 轮询 (poll):检索新的 I/O 事件; 执行与 I/O 相干的回调(简直所有状况下,除了敞开的回调函数,那些由计时器和 setImmediate() 调度的之外)* 检测 (check):setImmediate() 回调函数在这里执行。* 敞开的回调函数 (close callbacks):一些敞开的回调函数,如:socket.on('close', ...)。

nodejs 中也和 javascript 中一样存在着微工作(micro-task)、宏工作(macro-task),两个工作中执行的内容也有一部分的相似性

 微工作:promise 的 then 函数的回调、queueMicrotask、process.nextTick
宏工作: setTimeout、setInterval、io 事件、setImmediate、close 事件 

执行程序也和 javascript 中统一,先执行主线程的工作,而后接着执微工作,微工作执行实现再执行宏工作,具体的执行程序如下。

 微工作队列
next tick queue:process.nextTick
other tick queue:promise 的 then 函数、queueMicrotask

宏工作队列
timer queue: setTimeout、setInterval
poll queue: io 事件
check queue: setImmediate
close queue: close 事件 

理解完 nodejs 中事件循环的执行程序之后,一起来看看上面这道面试题

async function async1() {console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {console.log('async2')
}

console.log('script start')

setTimeout(function () {console.log('setTimeout0')
}, 0)

setTimeout(function () {console.log('setTimeout2')
}, 300)

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

process.nextTick(() => console.log('nextTick1'));

async1();

process.nextTick(() => console.log('nextTick2'));

new Promise(function (resolve) {console.log('promise1')
  resolve();
  console.log('promise2')
}).then(function () {console.log('promise3')
})

console.log('script end')

首先申明了 async1 和 async2 函数,只有调用才会被放入调用栈中,所以此时不会执行,往下执行输入 “script start”。

持续向下执行,将 “setTimeout0″ 放入 宏工作的 timer queue 中,继续执行,”setTimeout2” 须要提早 300ms 执行,不放入 timer queue 中,接着把 “setImmediate” 放入 check queue 中,把 “nextTick1” 放入 next tick queue 中。

此时执行函数 async1,输入 “async1 start”,在函数 async1 中执行 async2,输入 “async2″,把 “async1 end” 放入 other queue 当中,此时函数 async1 执行实现,持续向下执行,将 ”nextTick2″ 放入 next tick queue 中,排在 ”nextTick1″ 前面,持续输入 “promise1″,将 ”promise3″ 放在 other queue 中,排在 ”async1 end” 前面,输入 ”promise2″,最初输入 ”script end”,此时主线程的内容都执行实现。

来到微工作队列,微工作队列中先执行 next tick queue 中的内容,顺次输入 “nectTick1″、”nectTick2″,再执行 other queue 中的内容,顺次输入 ”async1 end”、”promise3″。

最初执行宏工作队列中的工作,先执行 timer queue,输入 “setTimeout0″,这里没有 io 事件(poll),往下执行 check quue,输入 setImmediate,没有敞开回调函数阶段 (close callbasks),一次事件循环完结,来到第二次、第三次事件循环,此时 300ms 后,”setTimeout2” 被退出到宏工作队列中的 timer queue,事件循环中没有其余的队列,间接输入 “setTimeout2″,事件循环完结。

简略图示如下

再来看另一道面试题

setTimeout(() => {console.log("setTimeout");
}, 0);

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

依照宏工作队列中各工作的执行程序,setTimeout 属于 timer queue,setImmediate 属于 check queue,按理说会先输入 setTimeout,但理论状况会是什么样的呢,咱们看一下以下输入状况

为什么会这样的状况呢,有时候是 setTimeout 先输入,有时候是 setImmediate 先输入,起因在于,setTimeout 的回调函数尽管是提早 0 毫秒执行,然而 setTimeout 的筹备工夫要长于 event loop 的启动工夫,当 event loop 开始第一次循环的时候,setTimeout 还没有被放入 timer queue 之中,所以 event loop 先执行 check queue 中的 setImmediate,期待第二次循环的时候,timer queue 中才有 setTimeout,此时就会呈现 setImmediate 先输入的状况。

nodejs 中的事件循环机制有一部分和 javascript 中事件循环是统一的,如果对 javascript 事件循环机制还不太熟悉,能够看看这一篇文章,javascript 事件循环机制及面试题详解

退出移动版