Node.js事件循环有没有把你搞懵逼掉😳?
前言
不知道大家有没有发现,浏览器的事件循环理解起来挺简单的,但是node的事件循环感觉理解起来就很费劲
来,我们先来一道面试题,你把这道面试题解释清楚,那么你就理解了node的事件循环了
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
错误答案:
timeout
immediate
和
immediate
timeout
正确答案: 以上的输出是不可预测的
在座的各位能解释出来为什么么?如果你能解释出来,那么下面的内容你不用看了,如果解释不出来,我还是希望大家能一起看完
事件循环的基本原理
Node.js 的事件循环(Event Loop)是其非阻塞I/O模型的核心机制,它使得Node.js能够高效地处理大量并发请求而无需创建额外的线程。事件循环遵循一套既定的阶段(Phase),并在每个阶段执行特定类型的回调函数。以下是Node.js事件循环的详细解释:
- 单线程执行: Node.js应用程序运行在一个单一的主线程中,这意味着任何JavaScript代码都是同步、按序执行的。当遇到异步操作(如文件I/O、网络请求、定时器等)时,Node.js不会等待这些操作完成,而是将它们交给libuv库(Node.js的C/C++底层库)处理,然后立即返回继续执行后续的同步代码。
- 异步操作与回调函数: 异步操作完成后,对应的回调函数不会立即执行,而是被放入一个任务队列(Task Queue)等待事件循环处理。这些回调函数分为不同的类型,根据其性质被安排在事件循环的不同阶段执行。
事件循环的六个阶段
Node.js事件循环通常包含以下六个阶段(顺序执行):
1. Timers(计时器)阶段
在此阶段,事件循环检查是否存在已到期的定时器回调(setTimeout() 和 setInterval())。如果存在,这些回调将按照它们预定的时间顺序被取出并执行。注意,尽管定时器回调在此阶段执行,但实际的定时器管理是由libuv库中的计时器轮询(timer wheel)实现的。
2. Pending Callbacks(待处理回调)阶段
这个阶段处理那些由某些系统操作(如TCP错误、某些流操作等)触发的回调。这些回调通常需要在其他阶段结束后立即得到处理,因此它们被安排在这个阶段执行。
3. Idle, Prepare 阶段(空闲/准备)
这两个阶段主要用于内部Node.js用途和libuv的准备任务,一般开发人员不需要直接与之交互。
4. Poll(轮询)阶段
Poll阶段是事件循环中的核心工作阶段,它负责处理I/O回调(如网络请求、文件系统操作等)。
5. Check(检查)阶段
此阶段用于执行setImmediate()回调。这些回调通常安排在当前轮询阶段结束后的下一次事件循环迭代开始时执行。
6. Close Callbacks(关闭回调)阶段
当诸如socket或handle这样的资源被关闭时,其关联的回调(如'socket.on('close', ...)')会被安排在这个阶段执行。
任务队列与微任务
微任务有哪些
主要记住下面这些就行了
- Promise.resolve()的回调
- process.nextTick()的回调
- async的回调(因为async,await是基于promise的)
微任务在每个事件循环阶段结束时执行,且在进入下一个阶段之前必须全部清空。这意味着,无论何时事件循环进入一个新的阶段之前,都会先处理完当前阶段产生的所有微任务。
事件循环流程总结
- 启动事件循环:Node.js应用程序开始时,初始化事件循环和相关组件。
- 执行同步代码:主线程按序执行JavaScript同步代码。
- 遇到异步操作:遇到异步API调用时,注册相应的回调函数,继续执行同步代码。
- 回调函数入队:异步操作完成后(比如延时,比如请求),对应的回调函数被放入相应的任务队列(普通任务队列或微任务队列)。
- 事件循环迭代:事件循环按照上述六个阶段顺序执行,每个阶段执行完后清空所有微任务。
- 重复步骤5:事件循环不断迭代,直至所有任务队列为空,程序结束。
通过这种机制,Node.js能够在单线程中有效地处理异步操作,避免了多线程编程中的复杂性,同时保持了高并发性和非阻塞性能。理解事件循环的工作原理对于编写高性能、可预测的Node.js应用程序至关重要。
以上都是一些概念,你先把概念读懂,后面的你也就明白了
setTimeout和setImmediate的执行顺序
为什么setTimeout和setImmediate的执行顺序不可预测
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
- 外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段
- 遇到
setTimeout
,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times
阶段- 遇到
setImmediate
塞入check
阶段- 同步代码执行完毕,进入Event Loop
- 先进入
times
阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout
条件,执行回调,如果没过1毫秒,跳过- 跳过空的阶段,进入check阶段,执行
setImmediate
回调
为什么在I/O里面setTimeout和setImmediate的执行顺序变的可预测
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
这里setTimeout
和setImmediate
在readFile
的回调里面,由于readFile
回调是I/O操作,他本身就在poll
阶段,所以他里面的定时器只能进入下个timers
阶段,但是setImmediate
却可以在接下来的check
阶段运行,所以setImmediate
肯定先运行,他运行完后,去检查timers
,才会运行setTimeout
。
总结
- 今天我们讲了
nodejs
事件循环,nodejs事件循环分为六个阶段,但是实际我们有感知的就三个阶段,他们的顺序分别为timer,poll,check阶段,每次执行完一个阶段就制定微任务,微任务先执行process.nextTick()
,然后在执行promise.resolve()
,因为process.nextTick()
优先级更高 - 之所以会有setTimeout和setImmediate的执行顺序问题,是因为node.js默认setTimeout执行时间为1ms,所以造成了他们的执行顺序通常是不可预测的
参考
转载自:https://juejin.cn/post/7356860329697214527