小编给大家分享一下JavaScript在nodejs环境下执行机制和事件循环的示例,希望大家阅读完这篇文章后大所收获,下面让我们一起去探讨吧!
1、说明
nodejs是单线程执行的,同时它又是基于事件驱动的非阻塞IO编程模型。这就使得我们不用等待异步操作结果返回,就可以继续往下执行代码。当异步事件触发之后,就会通知主线程,主线程执行相应事件的回调。
本篇文章讲解node中JavaScript的代码的执行流程,下面是测试代码,如果你知道输出的结果,那么就不需要再看本篇文章,如果不知道输出结果,那么本片文章可帮助你了解:
console.log(1) setTimeout(function () { new Promise(function (resolve) { console.log(2) resolve() }) .then(() => { console.log(3) }) }) setTimeout(function () { console.log(4) })
复杂的:
setTimeout(() => { console.log('1') new Promise((resolve) => { console.log('2'); resolve(); }) .then(() => { console.log('3') }) new Promise((resolve)=> { console.log('4'); resolve()}) .then(() => { console.log('5') }) setTimeout(() => { console.log('6') setTimeout(() => { console.log('7') new Promise((resolve) => { console.log('8'); resolve() }) .then( () => { console.log('9') }) new Promise((resolve) => { console.log('10'); resolve() }) .then(() => { console.log('11') }) }) setTimeout(() => { console.log('12') }, 0) }) setTimeout(() => { console.log('13') }, 0) }) setTimeout(() => { console.log('14') }, 0) new Promise((resolve) => { console.log('15'); resolve() }) .then( ()=> { console.log('16') }) new Promise((resolve) => { console.log('17'); resolve() }) .then(() => { console.log('18') })
2. nodejs的启动过程
node.js启动过程可以分为以下步骤:
3. nodejs的事件循环详解
Nodejs 将消息循环又细分为 6 个阶段(官方叫做 Phase), 每个阶段都会有一个类似于队列的结构, 存储着该阶段需要处理的回调函数.
Nodejs 为了防止某个 阶段 任务太多, 导致后续的 阶段 发生饥饿的现象, 所以消息循环的每一个迭代(iterate) 中, 每个 阶段 执行回调都有个最大数量. 如果超过数量的话也会强行结束当前 阶段而进入下一个 阶段. 这一条规则适用于消息循环中的每一个 阶段.
这是消息循环的第一个阶段, 用一个 for
循环处理所有 setTimeout
和 setInterval
的回调.
这些回调被保存在一个最小堆(min heap) 中. 这样引擎只需要每次判断头元素, 如果符合条件就拿出来执行, 直到遇到一个不符合条件或者队列空了, 才结束 Timer Phase.
Timer 阶段中判断某个回调是否符合条件的方法也很简单. 消息循环每次进入 Timer 的时候都会保存一下当时的系统时间,然后只要看上述最小堆中的回调函数设置的启动时间是否超过进入 Timer 时保存的时间, 如果超过就拿出来执行.
执行除了close callbacks
、setTimeout()
、setInterval()
、setImmediate()
回调之外几乎所有回调,比如说TCP连接发生错误
、 fs.read
, socket
等 IO 操作的回调函数, 同时也包括各种 error 的回调.
系统内部的一些调用。
这是整个消息循环中最重要的一个 阶段, 作用是等待异步请求和数据,因为它支撑了整个消息循环机制.
poll阶段有两个主要的功能:一是执行下限时间已经达到的timers的回调,一是处理poll队列里的事件。
注:Node的很多API都是基于事件订阅完成的,比如fs.readFile,这些回调应该都在poll
阶段完成。
当事件循环进入poll阶段:
poll
队列不为空的时候,事件循环肯定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。poll
队列为空的时候,这里有两种情况。
setImmediate()
设定了回调,那么事件循环直接结束poll
阶段进入check
阶段来执行check
队列里的回调。如果代码没有被设定setImmediate()
设定回调:
Poll阶段,当js层代码注册的事件回调都没有返回的时候,事件循环会暂时阻塞在poll阶段,解除阻塞的条件:
- 在poll阶段执行的时候,会传入一个timeout超时时间,该超时时间就是poll阶段的最大阻塞时间。
- timeout时间未到的时候,如果有事件返回,就执行该事件注册的回调函数。timeout超时时间到了,则退出poll阶段,执行下一个阶段。
这个 timeout 设置为多少合适呢? 答案就是 Timer Phase 中最近要执行的回调启动时间到现在的差值, 假设这个差值是 detal. 因为 Poll Phase 后面没有等待执行的回调了. 所以这里最多等待 delta 时长, 如果期间有事件唤醒了消息循环, 那么就继续下一个 Phase 的工作; 如果期间什么都没发生, 那么到了 timeout 后, 消息循环依然要进入后面的 Phase, 让下一个迭代的 Timer Phase 也能够得到执行.
Nodejs 就是通过 Poll Phase, 对 IO 事件的等待和内核异步事件的到达来驱动整个消息循环的.
这个阶段只处理 setImmediate 的回调函数.
那么为什么这里要有专门一个处理 setImmediate 的 阶段 呢? 简单来说, 是因为 Poll 阶段可能设置一些回调, 希望在 Poll 阶段 后运行. 所以在 Poll 阶段 后面增加了这个 Check 阶段.
专门处理一些 close 类型的回调. 比如 socket.on('close', ...)
. 用于资源清理.
1、node初始化
初始化node环境
执行输入的代码
执行process.nextTick
回调
执行微任务(microtasks)
2、进入事件循环
2.1、进入Timer
阶段
Timer
队列是否有到期的Timer
的回调,如果有,将到期的所有Timer
回调按照TimerId
升序执行process.nextTick
任务,如果有,全部执行2.2、进入Pending I/O Callback
阶段
Pending I/O Callback
的回调,如果有,执行回调。如果没有退出该阶段process.nextTick
任务,如果有,全部执行2.3、进入idle,prepare
阶段
这个阶段与JavaScript关系不大,略过
2.4、进入Poll
阶段
首先检查是否存在尚未完成的回调,如果存在,分如下两种情况:
第一种情况:有可执行的回调
执行所有可用回调(包含到期的定时器还有一些IO事件等)
检查是否有process.nextTick
任务,如果有,全部执行
检查是否有微任务(promise),如果有,全部执行
退出该阶段
第二种情况:没有可执行的回调
检查是否有immediate
回调,如果有,退出Poll阶段。如果没有,阻塞在此阶段,等待新的事件通知
如果不存在尚未完成的回调,退出Poll阶段
2.5、进入check
阶段
如果有immediate回调,则执行所有immediate回调
检查是否有process.nextTick
任务,如果有,全部执行
检查是否有微任务(promise),如果有,全部执行
退出该阶段
2.6、进入closing
阶段
如果有immediate回调,则执行所有immediate回调
检查是否有process.nextTick
任务,如果有,全部执行
检查是否有微任务(promise),如果有,全部执行
退出该阶段
3、检查是否有活跃的handles(定时器、IO等事件句柄)
如果有,继续下一轮事件循环
如果没有,结束事件循环,退出程序
注意:
事件循环的每一个子阶段退出之前都会按顺序执行如下过程:
检查是否有 process.nextTick 回调,如果有,全部执行。
检查是否有 微任务(promise),如果有,全部执行。
事件循环队列先保证所有的process.nextTick
回调,然后将所有的Promise
回调追加在后面,最终在每个阶段结束的时候一次性拿出来执行。
此外,process.nextTick
和Promise
回调的数量是受限制的,也就是说,如果一直往这个队列中加入回调,那么整个事件循环就会被卡住
。
这两个方法的回调到底谁快?
如下面的例子:
setImmediate(() => console.log(2)) setTimeout(() => console.log(1))
使用nodejs多次执行后,发现输出结果有时是1 2
,有时是2 1
。
对于多次执行输出结果不同,需要了解事件循环的基础问题。
首先,Nodejs启动,初始化环境后加载我们的JS代码(index.js).发生了两件事(此时尚未进入消息循环环节):
setImmediate 向 Check 阶段 中添加了回调 console.log(2);setTimeout 向 Timer 阶段 中添加了回调 console.log(1)
这时候, 要初始化阶段完毕, 要进入 Nodejs 消息循环了。
为什么会有两种输出呢? 接下来一步很关键:
当执行到 Timer 阶段 时, 会发生两种可能. 因为每一轮迭代刚刚进入 Timer 阶段 时会取系统时间保存起来, 以 ms(毫秒) 为最小单位.
如果 Timer 阶段 中回调预设的时间 > 消息循环所保存的时间, 则执行 Timer 阶段 中的该回调. 这种情况下先输出 1, 直到 Check 阶段 执行后,输出2.总的来说, 结果是 1 2.
如果运行比较快, Timer 阶段 中回调预设的时间可能刚好等于消息循环所保存的时间, 这种情况下, Timer 阶段 中的回调得不到执行, 则继续下一个 阶段. 直到 Check 阶段, 输出 2. 然后等下一轮迭代的 Timer 阶段, 这时的时间一定是满足 Timer 阶段 中回调预设的时间 > 消息循环所保存的时间 , 所以 console.log(1) 得到执行, 输出 1. 总的来说, 结果就是 2 1.
所以, 输出不稳定的原因就取决于进入 Timer 阶段 的时间是否和执行 setTimeout 的时间在 1ms 内. 如果把代码改成如下, 则一定会得到稳定的输出:
require('fs').readFile('my-file-path.txt', () => { setImmediate(() => console.log(2)) setTimeout(() => console.log(1)) });
这是因为消息循环在 Pneding I/O Phase
才向 Timer 和 Check 队列插入回调. 这时按照消息循环的执行顺序, Check 一定在 Timer 之前执行。
从性能角度讲, setTimeout 的处理是在 Timer Phase, 其中 min heap 保存了 timer 的回调, 因此每执行一个回调的同时都会涉及到堆调整. 而 setImmediate 仅仅是清空一个队列. 效率自然会高很多.
再从执行时机上讲. setTimeout(..., 0) 和 setImmediate 完全属于两个阶段.
下面以一段代码来说明nodejs运行JavaScript的机制。
如下面一段代码:
setTimeout(() => { // settimeout1 console.log('1') new Promise((resolve) => { console.log('2'); resolve(); }) // Promise3 .then(() => { console.log('3') }) new Promise((resolve)=> { console.log('4'); resolve()}) // Promise4 .then(() => { console.log('5') }) setTimeout(() => { // settimeout3 console.log('6') setTimeout(() => { // settimeout5 console.log('7') new Promise((resolve) => { console.log('8'); resolve() }) // Promise5 .then( () => { console.log('9') }) new Promise((resolve) => { console.log('10'); resolve() }) // Promise6 .then(() => { console.log('11') }) }) setTimeout(() => { console.log('12') }, 0) // settimeout6 }) setTimeout(() => { console.log('13') }, 0) // settimeout4 }) setTimeout(() => { console.log('14') }, 0) // settimeout2 new Promise((resolve) => { console.log('15'); resolve() }) // Promise1 .then( ()=> { console.log('16') }) new Promise((resolve) => { console.log('17'); resolve() }) // Promise2 .then(() => { console.log('18') })
上面代码执行过程:
node初始化
执行JavaScript代码
遇到setTimeout
, 把回调函数放到Timer
队列中,记为settimeout1
遇到setTimeout
, 把回调函数放到Timer
队列中,记为settimeout2
遇到Promise
,执行,输出15,把回调函数放到微任务
队列,记为Promise1
遇到Promise
,执行,输出17,把回调函数放到微任务
队列,记为Promise2
代码执行结束,此阶段输出结果:15 17
没有process.nextTick
回调,略过
执行微任务
检查微任务队列是否有可执行回调,此时队列有2个回调:Promise1、Promise2
执行Promise1回调,输出16
执行Promise2回调,输出18
此阶段输出结果:16 18
进入第一次事件循环
进入Timer阶段
检查Timer队列是否有可执行的回调,此时队列有2个回调:settimeout1、settimeout2
执行settimeout1回调:
输出1、2、4
添加了2个微任务,记为Promise3、Promise4
添加了2个Timer任务,记为settimeout3、settimeout4
执行settimeout2回调,输出14
Timer队列任务执行完毕
没有process.nextTick
回调,略过
检查微任务队列是否有可执行回调,此时队列有2个回调:Promise3、Promise4
按顺序执行2个微任务,输出3、5
此阶段输出结果:1 2 4 14 3 5
Pending I/O Callback阶段没有任务,略过
进入 Poll 阶段
检查是否存在尚未完成的回调,此时有2个回调:settimeout3、settimeout4
执行settimeout3回调
输出6
添加了2个Timer任务,记为settimeout5、settimeout6
执行settimeout4回调,输出13
没有process.nextTick
回调,略过
没有微任务,略过
此阶段输出结果:6 13
check、closing阶段没有任务,略过
检查是否还有活跃的handles(定时器、IO等事件句柄)
,有,继续下一轮事件循环
进入第二次事件循环
进入Timer阶段
检查Timer队列是否有可执行的回调,此时队列有2个回调:settimeout5、settimeout6
执行settimeout5回调:
输出7、 8、10
添加了2个微任务,记为Promise5、Promise6
执行settimeout6回调,输出12
没有process.nextTick
回调,略过
检查微任务队列是否有可执行回调,此时队列有2个回调:Promise5、Promise6
按顺序执行2个微任务,输出9、11
此阶段输出结果:7 8 10 12 9 11
Pending I/O Callback、Poll、check、closing阶段没有任务,略过
检查是否还有活跃的handles(定时器、IO等事件句柄)
,没有了,结束事件循环,退出程序
程序执行结束,输出结果:15 17 16 18 1 2 4 14 3 5 6 13 7 8 10 12 9 11
看完了这篇文章,相信你对JavaScript在nodejs环境下执行机制和事件循环的示例有了一定的了解,想了解更多相关知识,欢迎关注亿速云行业资讯频道,感谢各位的阅读!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。